From 642c2e6c4a2a8f9704fc37a4c88976ab1c58d723 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER <62715763+jujusb@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:26:09 +0200 Subject: [PATCH 1/4] first version demo with plotly --- ProcessxAPISGStatement.ipynb | 301 -- ProcessxAPISGStatement.py | 226 ++ T-Mon-SIMVA.ipynb | 176 - demo.ipynb | 3367 +++++++++++++++++ ...dUploadButtonToLoadProcessStatements.ipynb | 220 -- ...rAndUploadButtonToLoadProcessStatements.py | 122 + ipyauth-keycloak-demo.env | 5 - requirements.txt | 4 +- 8 files changed, 3718 insertions(+), 703 deletions(-) delete mode 100644 ProcessxAPISGStatement.ipynb create mode 100644 ProcessxAPISGStatement.py delete mode 100644 T-Mon-SIMVA.ipynb create mode 100644 demo.ipynb delete mode 100644 fileBrowserAndUploadButtonToLoadProcessStatements.ipynb create mode 100644 fileBrowserAndUploadButtonToLoadProcessStatements.py delete mode 100644 ipyauth-keycloak-demo.env diff --git a/ProcessxAPISGStatement.ipynb b/ProcessxAPISGStatement.ipynb deleted file mode 100644 index d0cf156..0000000 --- a/ProcessxAPISGStatement.ipynb +++ /dev/null @@ -1,301 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***process_xapisg_statement* function** receives an xAPI-SG statement and updates the dictionary of players information\n", - "\n", - "\n", - "Inputs:\n", - "* data : xAPI-SG statement\n", - "* players_info: dictionary with players info\n", - "* timeformats: list of timeformats for timestamp" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# template with default information for each player\n", - "template_player_info = {\n", - " \"game_started\": False, \"game_completed\": False,\n", - " \"interactions\":{}, # dict of interactions\n", - " \"game_progress_per_time\": [], # list of pairs (game progress, timestamp)\n", - " \"completables_scores\": {}, # dict completable : last score\n", - " \"completables_progress\": {}, # list of pairs completable : last progress\n", - " \"completables_times\": {}, # dict completable: (start, end)\n", - " \"alternatives\": {}, # dict alternative: list of pairs (response, correct (T/F))\n", - " \"action_type_interaction\":{}, # dict of action type interactions\n", - " \"accessible\":{}, # dict of accessible\n", - " \"videos_seen\": [], # list of videos seen (accessed) by player\n", - " \"videos_skipped\": [], # list of videos skipped by player\n", - " \"selected_menus\":{} # dict of menus and response selected\n", - "}\n", - "\n", - "def process_xapisg_statement(data, players_info, timeformats):\n", - " # available keys in statement\n", - " keys = data.keys()\n", - "\n", - " ## extracting fields from xAPI-SG statement\n", - " # actor field\n", - " if \"actor\" in keys:\n", - " if \"name\" in data[\"actor\"].keys():\n", - " actor_name = data[\"actor\"][\"name\"]\n", - "\n", - " if actor_name not in players_info.keys():\n", - " players_info[actor_name] = copy.deepcopy(template_player_info)\n", - "\n", - " player_info = players_info[actor_name]\n", - "\n", - " # verb field\n", - " if \"verb\" in keys:\n", - " if \"id\" in data[\"verb\"].keys():\n", - " verb_id = data[\"verb\"][\"id\"]\n", - " # process verb field\n", - " verb_xapi = np.array(verb_id.split(\"/\"))[-1]\n", - "\n", - " # object field\n", - " if \"object\" in keys:\n", - " if \"id\" in data[\"object\"].keys():\n", - " object_id = data[\"object\"][\"id\"]\n", - " # process object id field\n", - " object_id_name = np.array(object_id.split(\"/\"))[-1]\n", - " if \"definition\" in data[\"object\"].keys() and \"type\" in data[\"object\"][\"definition\"].keys():\n", - " object_type = data[\"object\"][\"definition\"][\"type\"]\n", - " # process object type field\n", - " object_type_xapi = np.array(object_type.split(\"/\"))[-1]\n", - " action_type=None\n", - " # result field\n", - " if \"result\" in keys:\n", - " if \"extensions\" in data[\"result\"].keys():\n", - " if \"response\" in data[\"result\"].keys():\n", - " result_response = data[\"result\"][\"response\"]\n", - " res = data[\"result\"][\"extensions\"]\n", - " else:\n", - " res = data[\"result\"]\n", - " if \"success\" in res.keys():\n", - " result_success = res[\"success\"]\n", - " if \"response\" in res.keys():\n", - " result_response = res[\"response\"]\n", - " if \"progress\" in res.keys():\n", - " result_progress = res[\"progress\"]\n", - " elif \"https://w3id.org/xapi/seriousgames/extensions/progress\" in res.keys():\n", - " result_progress = res[\"https://w3id.org/xapi/seriousgames/extensions/progress\"]\n", - " if \"score\" in res.keys():\n", - " if(isInstance(res[\"score\"],dict)):\n", - " result_score = res[\"score\"][\"raw\"]\n", - " else:\n", - " result_score = res[\"score\"]\n", - " if \"action_type\" in res.keys():\n", - " action_type=res[\"action_type\"]\n", - "\n", - " # timestamp field\n", - " if \"timestamp\" in keys:\n", - " timestamp = data[\"timestamp\"]\n", - " try:\n", - " t=timestampTotimedate(timestamp, timeformats)\n", - " except TimeFormatError as e:\n", - " print(e)\n", - " \n", - " ## update values\n", - " try:\n", - " # initialized traces\n", - " if verb_xapi.lower()==\"initialized\":\n", - " if object_type_xapi.lower()==\"serious-game\":\n", - " player_info[\"game_started\"] = True\n", - " if timestamp: player_info[\"game_progress_per_time\"].append([0,t])\n", - "\n", - " if timestamp: player_info[\"completables_times\"][object_id_name] = (t, 0) # (start, end) times\n", - "\n", - " # completed traces\n", - " elif verb_xapi.lower()==\"completed\":\n", - " if object_type_xapi.lower()==\"serious-game\":\n", - " player_info[\"game_completed\"] = True\n", - " if timestamp: player_info[\"game_progress_per_time\"].append([1,t])\n", - "\n", - " if timestamp and object_id_name in player_info[\"completables_times\"].keys():\n", - " prev = player_info[\"completables_times\"][object_id_name][0]\n", - " player_info[\"completables_times\"][object_id_name] = (prev, t) # update end time\n", - " if object_id_name and timestamp and result_score:\n", - " player_info[\"completables_scores\"][object_id_name] = result_score\n", - " \n", - " # progressed traces\n", - " elif verb_xapi.lower()==\"progressed\":\n", - " if object_type_xapi.lower()==\"serious-game\" and timestamp and result_progress:\n", - " player_info[\"game_progress_per_time\"].append([result_progress,t])\n", - " if result_progress==1:\n", - " player_info['game_completed'] = True\n", - " \n", - " if verb_xapi.lower()==\"progressed\" and object_id_name and result_progress:\n", - " if not object_id_name in player_info[\"completables_progress\"].keys():\n", - " player_info[\"completables_progress\"][object_id_name]=[]\n", - " player_info[\"completables_progress\"][object_id_name].append([result_progress,t])\n", - " \n", - " # interacted and used traces\n", - " elif verb_xapi.lower()==\"interacted\" or verb_xapi.lower()==\"used\":\n", - " if action_type!=None:\n", - " if not object_type_xapi in player_info[\"action_type_interaction\"].keys():\n", - " player_info[\"action_type_interaction\"][object_type_xapi]={}\n", - "\n", - " if not object_id_name in player_info[\"action_type_interaction\"][object_type_xapi].keys():\n", - " player_info[\"action_type_interaction\"][object_type_xapi][object_id_name]={}\n", - " \n", - " if not action_type in player_info[\"action_type_interaction\"][object_type_xapi][object_id_name].keys():\n", - " player_info[\"action_type_interaction\"][object_type_xapi][object_id_name][action_type]=[]\n", - " \n", - " player_info[\"action_type_interaction\"][object_type_xapi][object_id_name][action_type].append(t)\n", - " \n", - " else:\n", - " if not object_type_xapi in player_info[\"interactions\"].keys():\n", - " player_info[\"interactions\"][object_type_xapi]={}\n", - " \n", - " if not object_id_name in player_info[\"interactions\"][object_type_xapi].keys():\n", - " player_info[\"interactions\"][object_type_xapi][object_id_name]=[]\n", - " \n", - " player_info[\"interactions\"][object_type_xapi][object_id_name].append(t)\n", - " \n", - " # selected traces\n", - " elif verb_xapi.lower()==\"selected\":\n", - " if object_type_xapi.lower()==\"alternative\":\n", - " if object_id_name and result_response and result_success is not None: # success = false is valid!\n", - " if object_id_name in player_info[\"alternatives\"].keys():\n", - " player_info[\"alternatives\"][object_id_name].append((result_response, result_success))\n", - " else:\n", - " player_info[\"alternatives\"][object_id_name] = [(result_response, result_success)]\n", - " \n", - " elif object_type_xapi.lower()==\"menu\":\n", - " if result_response:\n", - " if not object_id_name in player_info[\"selected_menus\"].keys():\n", - " player_info[\"selected_menus\"][object_id_name]={}\n", - " if not result_response in player_info[\"selected_menus\"][object_id_name].keys():\n", - " player_info[\"selected_menus\"][object_id_name][result_response]=[]\n", - " if timestamp:\n", - " t=timestampTotimedate(timestamp, timeformats)\n", - " player_info[\"selected_menus\"][object_id_name][result_response].append(t)\n", - " # accessed traces\n", - " elif verb_xapi.lower()==\"accessed\":\n", - " if object_type_xapi.lower()==\"cutscene\" and object_id_name:\n", - " player_info[\"videos_seen\"].append(object_id_name)\n", - " elif object_type_xapi.lower()==\"accessible\" and object_id_name:\n", - " if not object_id_name in player_info[\"accessible\"].keys():\n", - " player_info[\"accessible\"][object_id_name]=[]\n", - " t=timestampTotimedate(timestamp, timeformats)\n", - " player_info[\"accessible\"][object_id_name].append(t)\n", - " # skipped traces\n", - " elif verb_xapi.lower()==\"skipped\":\n", - " if object_type_xapi.lower()==\"cutscene\" and object_id_name:\n", - " player_info[\"videos_skipped\"].append(object_id_name)\n", - " except NameError:\n", - " pass#%% md" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***timestampTotimedate* function** to tranform the timestamp in datetime\n", - "Inputs:\n", - "* timestamp : the time in a string\n", - "* timeformats : an array of timeformat witch match the timestamp with\n", - "\n", - "Output:\n", - "* return t : dateTime\n", - "* raise TimeFormatError : in case of the timestamp not in timeformat array" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def timestampTotimedate(timestamp, timeformats):\n", - " t=None\n", - " for timeformat in timeformats:\n", - " try:\n", - " t = datetime.strptime(timestamp, timeformat)\n", - " except ValueError:\n", - " pass\n", - " if t==None:\n", - " str=\"TimeFormatError : This timestamp don't match with formats in \"\n", - " for format in timeformats:\n", - " str+=format+\" \"\n", - " raise TimeFormatError(timestamp, str)\n", - " else:\n", - " return t" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***TimeFormatError* class** to raise an error in case of the timestamp not in timeformat array " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class Error(Exception):\n", - " \"\"\"Base class for exceptions in this module.\"\"\"\n", - " pass\n", - "\n", - "class TimeFormatError(Error):\n", - " \"\"\"Exception raised for errors in timeformat.\n", - "\n", - " Attributes:\n", - " expression -- input expression in which the error occurred\n", - " message -- explanation of the error\n", - " \"\"\"\n", - "\n", - " def __init__(self, expression, message):\n", - " self.expression = expression\n", - " self.message = message" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/ProcessxAPISGStatement.py b/ProcessxAPISGStatement.py new file mode 100644 index 0000000..2325f91 --- /dev/null +++ b/ProcessxAPISGStatement.py @@ -0,0 +1,226 @@ +# imports from actually useful code +import numpy as np +import copy +import math +import datetime + +# +# ProcessxAPISGStatement.ipynb +# +#***timestampTotimedate* function** to tranform the timestamp in datetime +#Inputs: +#* timestamp : the time in a string +#* timeformats : an array of timeformat witch match the timestamp with +# +#Output: +#* return t : dateTime +#* raise TimeFormatError : in case of the timestamp not in timeformat array +def timestampTotimedate(timestamp, timeformats): + t=None + for timeformat in timeformats: + try: + t = datetime.datetime.strptime(timestamp, timeformat) + except ValueError: + pass + if t==None: + str="TimeFormatError : This timestamp don't match with formats in " + for format in timeformats: + str+=format+" " + raise TimeFormatError(timestamp, str) + else: + return t + +class Error(Exception): + """Base class for exceptions in this module.""" + pass + +class TimeFormatError(Error): + """Exception raised for errors in timeformat. + + Attributes: + expression -- input expression in which the error occurred + message -- explanation of the error + """ + + def __init__(self, expression, message): + self.expression = expression + self.message = message + +# template with default information for each player +template_player_info = { + "game_started": False, "game_completed": False, + "interactions":{}, # dict of interactions + "game_progress_per_time": [], # list of pairs (game progress, timestamp) + "completables_scores": {}, # dict completable : last score + "completables_progress": {}, # list of pairs completable : last progress + "completables_times": {}, # dict completable: (start, end) + "alternatives": {}, # dict alternative: list of pairs (response, correct (T/F)) + "action_type_interaction":{}, # dict of action type interactions + "accessible":{}, # dict of accessible + "videos_seen": [], # list of videos seen (accessed) by player + "videos_skipped": [], # list of videos skipped by player + "selected_menus":{} # dict of menus and response selected +} + +#***process_xapisg_statement* function** receives an xAPI-SG statement and updates the dictionary of players information +# +# +#Inputs: +#* data : xAPI-SG statement +#* players_info: dictionary with players info +#* timeformats: list of timeformats for timestamp +def process_xapisg_statement(data, players_info, timeformats): + # available keys in statement + keys = data.keys() + + ## extracting fields from xAPI-SG statement + # actor field + if "actor" in keys: + if "name" in data["actor"].keys(): + actor_name = data["actor"]["name"] + + if actor_name not in players_info.keys(): + players_info[actor_name] = copy.deepcopy(template_player_info) + + player_info = players_info[actor_name] + + # verb field + if "verb" in keys: + if "id" in data["verb"].keys(): + verb_id = data["verb"]["id"] + # process verb field + verb_xapi = np.array(verb_id.split("/"))[-1] + + # object field + if "object" in keys: + if "id" in data["object"].keys(): + object_id = data["object"]["id"] + # process object id field + object_id_name = np.array(object_id.split("/"))[-1] + if "definition" in data["object"].keys() and "type" in data["object"]["definition"].keys(): + object_type = data["object"]["definition"]["type"] + # process object type field + object_type_xapi = np.array(object_type.split("/"))[-1] + action_type=None + # result field + if "result" in keys: + if "extensions" in data["result"].keys(): + if "response" in data["result"].keys(): + result_response = data["result"]["response"] + res = data["result"]["extensions"] + else: + res = data["result"] + if "success" in res.keys(): + result_success = res["success"] + if "response" in res.keys(): + result_response = res["response"] + if "progress" in res.keys(): + result_progress = res["progress"] + elif "https://w3id.org/xapi/seriousgames/extensions/progress" in res.keys(): + result_progress = res["https://w3id.org/xapi/seriousgames/extensions/progress"] + if "score" in res.keys(): + if(isinstance(res["score"],dict)): + result_score = res["score"]["raw"] + else: + result_score = res["score"] + if "action_type" in res.keys(): + action_type=res["action_type"] + + # timestamp field + if "timestamp" in keys: + timestamp = data["timestamp"] + try: + t=timestampTotimedate(timestamp, timeformats) + except TimeFormatError as e: + print(e) + + ## update values + try: + # initialized traces + if verb_xapi.lower()=="initialized": + if object_type_xapi.lower()=="serious-game": + player_info["game_started"] = True + if timestamp: player_info["game_progress_per_time"].append([0,t]) + + if timestamp: player_info["completables_times"][object_id_name] = (t, 0) # (start, end) times + + # completed traces + elif verb_xapi.lower()=="completed": + if object_type_xapi.lower()=="serious-game": + player_info["game_completed"] = True + if timestamp: player_info["game_progress_per_time"].append([1,t]) + + if timestamp and object_id_name in player_info["completables_times"].keys(): + prev = player_info["completables_times"][object_id_name][0] + player_info["completables_times"][object_id_name] = (prev, t) # update end time + if object_id_name and timestamp and result_score: + player_info["completables_scores"][object_id_name] = result_score + + # progressed traces + elif verb_xapi.lower()=="progressed": + if object_type_xapi.lower()=="serious-game" and timestamp and result_progress: + player_info["game_progress_per_time"].append([result_progress,t]) + if result_progress==1: + player_info['game_completed'] = True + + if verb_xapi.lower()=="progressed" and object_id_name and result_progress: + if not object_id_name in player_info["completables_progress"].keys(): + player_info["completables_progress"][object_id_name]=[] + player_info["completables_progress"][object_id_name].append([result_progress,t]) + + # interacted and used traces + elif verb_xapi.lower()=="interacted" or verb_xapi.lower()=="used": + if action_type!=None: + if not object_type_xapi in player_info["action_type_interaction"].keys(): + player_info["action_type_interaction"][object_type_xapi]={} + + if not object_id_name in player_info["action_type_interaction"][object_type_xapi].keys(): + player_info["action_type_interaction"][object_type_xapi][object_id_name]={} + + if not action_type in player_info["action_type_interaction"][object_type_xapi][object_id_name].keys(): + player_info["action_type_interaction"][object_type_xapi][object_id_name][action_type]=[] + + player_info["action_type_interaction"][object_type_xapi][object_id_name][action_type].append(t) + + else: + if not object_type_xapi in player_info["interactions"].keys(): + player_info["interactions"][object_type_xapi]={} + + if not object_id_name in player_info["interactions"][object_type_xapi].keys(): + player_info["interactions"][object_type_xapi][object_id_name]=[] + + player_info["interactions"][object_type_xapi][object_id_name].append(t) + + # selected traces + elif verb_xapi.lower()=="selected": + if object_type_xapi.lower()=="alternative": + if object_id_name and result_response and result_success is not None: # success = false is valid! + if object_id_name in player_info["alternatives"].keys(): + player_info["alternatives"][object_id_name].append((result_response, result_success)) + else: + player_info["alternatives"][object_id_name] = [(result_response, result_success)] + + elif object_type_xapi.lower()=="menu": + if result_response: + if not object_id_name in player_info["selected_menus"].keys(): + player_info["selected_menus"][object_id_name]={} + if not result_response in player_info["selected_menus"][object_id_name].keys(): + player_info["selected_menus"][object_id_name][result_response]=[] + if timestamp: + t=timestampTotimedate(timestamp, timeformats) + player_info["selected_menus"][object_id_name][result_response].append(t) + # accessed traces + elif verb_xapi.lower()=="accessed": + if object_type_xapi.lower()=="cutscene" and object_id_name: + player_info["videos_seen"].append(object_id_name) + elif object_type_xapi.lower()=="accessible" and object_id_name: + if not object_id_name in player_info["accessible"].keys(): + player_info["accessible"][object_id_name]=[] + t=timestampTotimedate(timestamp, timeformats) + player_info["accessible"][object_id_name].append(t) + # skipped traces + elif verb_xapi.lower()=="skipped": + if object_type_xapi.lower()=="cutscene" and object_id_name: + player_info["videos_skipped"].append(object_id_name) + except NameError: + pass#%% md diff --git a/T-Mon-SIMVA.ipynb b/T-Mon-SIMVA.ipynb deleted file mode 100644 index eee3b18..0000000 --- a/T-Mon-SIMVA.ipynb +++ /dev/null @@ -1,176 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# T-Mon: Traces Monitor in xAPI-SG, with SIMVA integration" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### T-Mon is a set of Jupyter Notebooks to process, analyze and present visualizations of data following the Experience API for Serious Games Profile (xAPI-SG)\n", - "\n", - "The following code:\n", - "- expects as input a **JSON file with a list of xAPI-SG statements, obtained from SIMVA**\n", - "- **analyzes the xAPI-SG statements** and fills an adaptation of the **default set of visualizations**\n", - "https://github.com/e-ucm/rage-analytics/wiki/Default-visualizations-teacher\n", - "\n", - "For more information, see **T-Mon GitHub page: https://github.com/e-ucm/t-mon**\n", - "\n", - "\n", - "\n", - "**xAPI-SG main reference:**\n", - "*Applying standards to systematize learning analytics in serious games.\n", - "Ángel Serrano-Laguna, Iván Martínez-Ortiz, Jason Haag, Damon Regan, Andy Johnson, Baltasar Fernández-Manjón\n", - "Computer Standards & Interfaces 50 (2017) 116–123, http://dx.doi.org/10.1016/j.csi.2016.09.014*\n", - "\n", - "Further info on GitHub wiki page: https://github.com/e-ucm/rage-analytics/wiki/xAPI-SG-Profile\n", - "\n", - "Further info about SIMVA on: https://github.com/e-ucm/simva-infra" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SIMVA connection parameters \n", - "\n", - "### Defined in *ipyauth-keycloak-demo.env* file:\n", - "\n", - "- `base_url`: URL pointing to SIMVA's SSO service (default: https://sso.simva.e-ucm.es)\n", - "- `realm`: simva\n", - "- `response_type`: id_token token\n", - "- `client_id`: SIMVA's client used by jupyter (e.g. jupyter)\n", - "- `redirect_uri`: jupyter URL (e.g. http://localhost:8888/callback/)\n", - "\n", - "### After running the following cell, click on \"Sign in\" to enter your SIMVA credentials" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "import requests as rq\n", - "\n", - "from ipyauth import Auth, ParamsKeycloak\n", - "\n", - "p = ParamsKeycloak(dotenv_file='ipyauth-keycloak-demo.env',\n", - " scope='profile openid email')\n", - "a = Auth(params=p)\n", - "a" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SIMVA storage parameters\n", - "\n", - "- `simva_storage_url`: URL pointing to SIMVA storage service (default: https://minio.simva.e-ucm.es)\n", - "- `simva_ca_file`: if SIMVA is installed locally this should point to a file with the CA certificate used by simva (usually located at `$SIMVA_HOME/docker-stacks/data/tls/ca/rootCA.pem`), `None` otherwise" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "simva_storage_url = 'https://minio.simva.e-ucm.es'\n", - "simva_ca_file = None\n", - "\n", - "local = True # Set to True if working with a local-hoster Jupyter server, False if working with a web-hosted Jupyter server\n", - "storage = 'simva'\n", - "\n", - "import json\n", - "import numpy as np\n", - "from datetime import datetime, timedelta\n", - "import copy\n", - "import math\n", - "import matplotlib.dates as mdates\n", - "import matplotlib.pyplot as plt\n", - "from collections import Counter\n", - "import pandas as pd\n", - "from ipywidgets import interact, interactive, fixed, HBox, Layout,VBox\n", - "import ipywidgets as widgets\n", - "from IPython.display import display, clear_output\n", - "import pprint\n", - "plt.style.use('default')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***Display all* function** is constructing an app to display all vis" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "players_info = {} # dict with players info\n", - "global timeformats\n", - "timeformats=['%Y-%m-%dT%H:%M:%SZ','%Y-%m-%dT%H:%M:%S.%fZ'] #array of time format\n", - "\n", - "%run fileBrowserAndUploadButtonToLoadProcessStatements.ipynb\n", - "%run vis/helpersFunctions/clearFigMatplotlib.ipynb\n", - "%run globalsSelectors.ipynb\n", - "\n", - "header=widgets.HTML(\"

xAPI-SG Processor

Please select .json xAPI SG file to process this file and see visualisations

\")\n", - "footer=widgets.HTML(\"

xAPI-SG Processor, by eUCM research team

\")\n", - "if storage == 'simva' :\n", - " app=VBox([header,simvaWidget.widget(),footer])\n", - " display(app)\n", - "else:\n", - " if local:\n", - " app=VBox([header,fileBrowser.widget(),footer])\n", - " display(app)\n", - " else:\n", - " app=VBox([header,uploadButtonApp,footer])\n", - " display(app)\n", - " with outTabs:\n", - " display_checkboxes()\n", - " clear_output(wait=True)\n", - " displayvis(None)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/demo.ipynb b/demo.ipynb new file mode 100644 index 0000000..4f21788 --- /dev/null +++ b/demo.ipynb @@ -0,0 +1,3367 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Info log (2 lines):\n", + " ... 1st line is valid JSON; interpreting as one-statement-per-line (84 statement(s))\n", + "... processed 84/84 statement(s) in 0:00:00.002255. Displaying visualizations ...\n" + ] + } + ], + "source": [ + "# cargamos un fichero con trazas, usando código de t-mon\n", + "\n", + "%run ProcessxAPISGStatement.py\n", + "%run fileBrowserAndUploadButtonToLoadProcessStatements.py\n", + "global players_info\n", + "players_info= load_players_info_from_file(\"xapi-sg-sample-data.json\")\n", + "players_info" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# cargamos pandas, plotly\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "import plotly.graph_objs as go\n", + "from plotly.subplots import make_subplots\n", + "from plotly.offline import init_notebook_mode, iplot\n", + "init_notebook_mode(connected=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "object of type 'NoneType' has no len()", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m~\\AppData\\Local\\Temp\\ipykernel_27168\\2959794928.py\u001b[0m in \u001b[0;36m?\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m# montemos un dataframe\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mdf\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpd\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mplayers_info\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'index'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[0mdf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mloc\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'John Smith'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32mc:\\github\\.venv\\Lib\\site-packages\\pandas\\core\\frame.py\u001b[0m in \u001b[0;36m?\u001b[1;34m(cls, data, orient, dtype, columns)\u001b[0m\n\u001b[0;32m 1894\u001b[0m \"\"\"\n\u001b[0;32m 1895\u001b[0m \u001b[0mindex\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1896\u001b[0m \u001b[0morient\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0morient\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# type: ignore[assignment]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1897\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0morient\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m\"index\"\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m-> 1898\u001b[1;33m \u001b[1;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1899\u001b[0m \u001b[1;31m# TODO speed up Series case\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1900\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0miter\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mSeries\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1901\u001b[0m \u001b[0mdata\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0m_from_nested_dict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mTypeError\u001b[0m: object of type 'NoneType' has no len()" + ] + } + ], + "source": [ + "# montemos un dataframe\n", + "df = pd.DataFrame.from_dict(players_info, 'index')\n", + "df.loc['John Smith']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total de 5 actores\n" + ] + } + ], + "source": [ + "# algo de información básica\n", + "print(f\"total de {len(df)} actores\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Intentemos ver puntuación en cada completable..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# montamos un dataframe para valores de este gráfico en concreto\n", + "cvalues = []\n", + "for id, row in df[['completables_scores']].iterrows():\n", + " for k, v in row['completables_scores'].items():\n", + " cvalues.append({'id': id, 'completable': k, 'score': v})\n", + "cvalues = pd.DataFrame.from_records(cvalues)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "name": "World1-1", + "type": "bar", + "x": [ + "John Smith", + "Sarah Connor", + "Juan Gomez" + ], + "y": [ + 50, + 17, + 22 + ] + }, + { + "name": "MyFirstGame", + "type": "bar", + "x": [ + "John Smith", + "Sarah Connor", + "James Dean", + "Mary Kate", + "Juan Gomez" + ], + "y": [ + 50, + 33, + 95, + 72, + 43 + ] + }, + { + "name": "World1-2", + "type": "bar", + "x": [ + "James Dean", + "Mary Kate", + "Juan Gomez" + ], + "y": [ + 95, + 72, + 60 + ] + } + ], + "layout": { + "barmode": "stack", + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Puntuación en completables" + }, + "xaxis": { + "categoryorder": "total descending" + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# mostramos\n", + "data = []\n", + "for c in cvalues['completable'].unique():\n", + " bar_data = cvalues[cvalues['completable'] == c]\n", + " data.append(go.Bar(x=bar_data['id'], y=bar_data['score'], name=c)) \n", + "fig = go.Figure(\n", + " layout_title_text=\"Puntuación en completables\",\n", + " data=data\n", + ")\n", + "fig.update_xaxes(categoryorder=\"total descending\")\n", + "fig.update_layout(barmode='stack')\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Y ahora, veamos progreso en el tiempo, con y sin alinear al inicio del experimento" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idcompletableprogresstime
0John SmithMyFirstGame0.152016-05-24 15:05:49
1John SmithMyFirstGame0.502016-05-24 15:15:49
2John SmithMyFirstGame0.702016-05-24 15:25:49
3John SmithMyFirstGame0.902016-05-24 15:35:49
4Sarah ConnorMyFirstGame0.102016-05-24 15:05:49
5Sarah ConnorMyFirstGame0.352016-05-24 15:15:49
6Sarah ConnorMyFirstGame0.552016-05-24 15:25:49
7Sarah ConnorMyFirstGame0.752016-05-24 15:35:49
8James DeanMyFirstGame0.452016-05-24 15:05:49
9James DeanMyFirstGame0.802016-05-24 15:15:49
10James DeanMyFirstGame0.992016-05-24 15:35:49
11Mary KateMyFirstGame0.252016-05-24 15:36:49
12Mary KateMyFirstGame0.352016-05-24 15:38:49
13Mary KateMyFirstGame0.652016-05-24 15:45:49
14Mary KateMyFirstGame0.752016-05-24 15:47:49
15Juan GomezMyFirstGame0.302016-05-24 15:17:49
16Juan GomezMyFirstGame0.502016-05-24 15:25:49
17Juan GomezMyFirstGame0.752016-05-24 15:33:49
18Juan GomezMyFirstGame0.952016-05-24 15:40:49
\n", + "
" + ], + "text/plain": [ + " id completable progress time\n", + "0 John Smith MyFirstGame 0.15 2016-05-24 15:05:49\n", + "1 John Smith MyFirstGame 0.50 2016-05-24 15:15:49\n", + "2 John Smith MyFirstGame 0.70 2016-05-24 15:25:49\n", + "3 John Smith MyFirstGame 0.90 2016-05-24 15:35:49\n", + "4 Sarah Connor MyFirstGame 0.10 2016-05-24 15:05:49\n", + "5 Sarah Connor MyFirstGame 0.35 2016-05-24 15:15:49\n", + "6 Sarah Connor MyFirstGame 0.55 2016-05-24 15:25:49\n", + "7 Sarah Connor MyFirstGame 0.75 2016-05-24 15:35:49\n", + "8 James Dean MyFirstGame 0.45 2016-05-24 15:05:49\n", + "9 James Dean MyFirstGame 0.80 2016-05-24 15:15:49\n", + "10 James Dean MyFirstGame 0.99 2016-05-24 15:35:49\n", + "11 Mary Kate MyFirstGame 0.25 2016-05-24 15:36:49\n", + "12 Mary Kate MyFirstGame 0.35 2016-05-24 15:38:49\n", + "13 Mary Kate MyFirstGame 0.65 2016-05-24 15:45:49\n", + "14 Mary Kate MyFirstGame 0.75 2016-05-24 15:47:49\n", + "15 Juan Gomez MyFirstGame 0.30 2016-05-24 15:17:49\n", + "16 Juan Gomez MyFirstGame 0.50 2016-05-24 15:25:49\n", + "17 Juan Gomez MyFirstGame 0.75 2016-05-24 15:33:49\n", + "18 Juan Gomez MyFirstGame 0.95 2016-05-24 15:40:49" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# montamos un dataframe para valores de este gráfico en concreto\n", + "tvalues = []\n", + "for id, row in df[['completables_progress']].iterrows():\n", + " for v in row['completables_progress']['MyFirstGame']:\n", + " tvalues.append({'id': id, 'completable': 'MyFirstGame', 'progress': v[0], 'time': np.datetime64(v[1])})\n", + "tvalues = pd.DataFrame.from_records(tvalues)\n", + "tvalues" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertext": "John Smith", + "mode": "lines+markers", + "name": "John Smith", + "type": "scatter", + "x": [ + "2016-05-24T15:05:49", + "2016-05-24T15:15:49", + "2016-05-24T15:25:49", + "2016-05-24T15:35:49" + ], + "y": [ + 0.15, + 0.5, + 0.7, + 0.9 + ] + }, + { + "hovertext": "Sarah Connor", + "mode": "lines+markers", + "name": "Sarah Connor", + "type": "scatter", + "x": [ + "2016-05-24T15:05:49", + "2016-05-24T15:15:49", + "2016-05-24T15:25:49", + "2016-05-24T15:35:49" + ], + "y": [ + 0.1, + 0.35, + 0.55, + 0.75 + ] + }, + { + "hovertext": "James Dean", + "mode": "lines+markers", + "name": "James Dean", + "type": "scatter", + "x": [ + "2016-05-24T15:05:49", + "2016-05-24T15:15:49", + "2016-05-24T15:35:49" + ], + "y": [ + 0.45, + 0.8, + 0.99 + ] + }, + { + "hovertext": "Mary Kate", + "mode": "lines+markers", + "name": "Mary Kate", + "type": "scatter", + "x": [ + "2016-05-24T15:36:49", + "2016-05-24T15:38:49", + "2016-05-24T15:45:49", + "2016-05-24T15:47:49" + ], + "y": [ + 0.25, + 0.35, + 0.65, + 0.75 + ] + }, + { + "hovertext": "Juan Gomez", + "mode": "lines+markers", + "name": "Juan Gomez", + "type": "scatter", + "x": [ + "2016-05-24T15:17:49", + "2016-05-24T15:25:49", + "2016-05-24T15:33:49", + "2016-05-24T15:40:49" + ], + "y": [ + 0.3, + 0.5, + 0.75, + 0.95 + ] + } + ], + "layout": { + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Progreso en 1er completable" + }, + "xaxis": { + "categoryorder": "total descending" + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# mostramos\n", + "data = []\n", + "for id in tvalues['id'].unique():\n", + " bar_data = tvalues[tvalues['id'] == id]\n", + " data.append(go.Scatter(x=bar_data['time'], y=bar_data['progress'], name=id, hovertext=f\"{id}\", mode=\"lines+markers\")) \n", + "fig = go.Figure(\n", + " layout_title_text=\"Progreso en 1er completable\",\n", + " data=data\n", + ")\n", + "fig.update_xaxes(categoryorder=\"total descending\")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertext": "John Smith", + "mode": "lines+markers", + "name": "John Smith", + "type": "scatter", + "x": [ + "1970-01-01T00:00:00", + "1970-01-01T00:10:00", + "1970-01-01T00:20:00", + "1970-01-01T00:30:00" + ], + "y": [ + 0.15, + 0.5, + 0.7, + 0.9 + ] + }, + { + "hovertext": "Sarah Connor", + "mode": "lines+markers", + "name": "Sarah Connor", + "type": "scatter", + "x": [ + "1970-01-01T00:00:00", + "1970-01-01T00:10:00", + "1970-01-01T00:20:00", + "1970-01-01T00:30:00" + ], + "y": [ + 0.1, + 0.35, + 0.55, + 0.75 + ] + }, + { + "hovertext": "James Dean", + "mode": "lines+markers", + "name": "James Dean", + "type": "scatter", + "x": [ + "1970-01-01T00:00:00", + "1970-01-01T00:10:00", + "1970-01-01T00:30:00" + ], + "y": [ + 0.45, + 0.8, + 0.99 + ] + }, + { + "hovertext": "Mary Kate", + "mode": "lines+markers", + "name": "Mary Kate", + "type": "scatter", + "x": [ + "1970-01-01T00:00:00", + "1970-01-01T00:02:00", + "1970-01-01T00:09:00", + "1970-01-01T00:11:00" + ], + "y": [ + 0.25, + 0.35, + 0.65, + 0.75 + ] + }, + { + "hovertext": "Juan Gomez", + "mode": "lines+markers", + "name": "Juan Gomez", + "type": "scatter", + "x": [ + "1970-01-01T00:00:00", + "1970-01-01T00:08:00", + "1970-01-01T00:16:00", + "1970-01-01T00:23:00" + ], + "y": [ + 0.3, + 0.5, + 0.75, + 0.95 + ] + } + ], + "layout": { + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Progreso en 1er completable, desde inicio de sesión" + }, + "xaxis": { + "categoryorder": "total descending" + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# y ahora, alineando por 1er evento\n", + "data = []\n", + "for id in tvalues['id'].unique():\n", + " bar_data = tvalues[tvalues['id'] == id].copy()\n", + " first = bar_data.time.min()\n", + " bar_data.time = pd.to_timedelta(bar_data.time - first) + pd.to_datetime('1970/01/01')\n", + " data.append(go.Scatter(x=bar_data['time'], y=bar_data['progress'], name=id, hovertext=f\"{id}\", mode=\"lines+markers\")) \n", + "fig = go.Figure(\n", + " layout_title_text=\"Progreso en 1er completable, desde inicio de sesión\",\n", + " data=data\n", + ")\n", + "fig.update_xaxes(categoryorder=\"total descending\")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------------------------------------------------------------------------\n", + "TypeError Traceback (most recent call last)\n", + "Cell In[32], line 55, in update_output(\n", + " list_of_contents=['data:application/json;base64,eyJhY3RvciI6eyJuYW1...sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0K'],\n", + " list_of_names=['xapi-sg-sample-data.json'],\n", + " list_of_dates=[1715587109.421]\n", + ")\n", + " 48 @callback(Output('output-data-upload', 'children'),\n", + " 49 Input('upload-data', 'contents'),\n", + " 50 State('upload-data', 'filename'),\n", + " 51 State('upload-data', 'last_modified'))\n", + " 52 def update_output(list_of_contents, list_of_names, list_of_dates):\n", + " 53 if list_of_contents is not None:\n", + " 54 return [\n", + "---> 55 fileBrowserAndUploadButtonToLoadProcessStatements.load_players_info_from_content(c, n, d) for c, n, d in\n", + " c = np.str_('World1-2')\n", + " list_of_contents = ['data:application/json;base64,eyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuMTV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC43fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjl9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTozNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL2V4dC92YWx1ZSI6IkdhbWUgT3ZlciIsInN1Y2Nlc3MiOiJUcnVlIiwic2NvcmUiOjUwfX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NDA6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IlRydWUiLCJzY29yZSI6NTB9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9TY3JlZW5zL1NvdW5kTWVudSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL1NjcmVlbiJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9jdXRzY2VuZXMvSW50cm9WaWRlbyIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0N1dHNjZW5lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9WaWRlb1R1dG9yaWFsIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2lkLnRpbmNhbmFwaS5jb20vdmVyYi9za2lwcGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9DdXRzY2VuZXMvSW50cm9WaWRlbyIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0N1dHNjZW5lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA5OjE3OjM3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mU3BhaW4iLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiTGlzYm9uIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWRyaWQiLCJzdWNjZXNzIjoiVHJ1ZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNaWFtaSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJPcmxhbmRvIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6IlRhbXBhIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6IlRhbGxhaGFzc2VlIiwic3VjY2VzcyI6IlRydWUifSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxMzowNToxMloifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9pbml0aWFsaXplZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjA1OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDU6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0xIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuMX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjA1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjF9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC4zNX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjU1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNzV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTozNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9jb21wbGV0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IkZhbHNlIiwic2NvcmUiOjE3fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NDg6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiRmFsc2UiLCJzY29yZSI6MzN9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo1NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL1NjcmVlbnMvU291bmRNZW51IiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvU2NyZWVuIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9hY2Nlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvY3V0c2NlbmVzL0ludHJvVmlkZW8iLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9DdXRzY2VuZSJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9WaWRlb1R1dG9yaWFsIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vaWQudGluY2FuYXBpLmNvbS92ZXJiL3NraXBwZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDk6MTc6MzdaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZlNwYWluIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ikxpc2JvbiIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWxsb3JjYSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ik1pYW1pIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZkZsb3JpZGEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiT3JsYW5kbyIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6IlRhbXBhIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZkZsb3JpZGEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiSmFja3NvbnZpbGxlIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTIiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNDV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuOH19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC45OX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjM1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9jb21wbGV0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IlRydWUiLCJzY29yZSI6OTV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTozNzoxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiVHJ1ZSIsInNjb3JlIjo5NX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjM3OjEzWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL1NjcmVlbnMvU291bmRNZW51IiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvU2NyZWVuIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9hY2Nlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvY3V0c2NlbmVzL1ZpZGVvVHV0b3JpYWwiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9DdXRzY2VuZSJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vaWQudGluY2FuYXBpLmNvbS92ZXJiL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9DdXRzY2VuZXMvVmlkZW9EZW1vIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDk6MTc6MzdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWRyaWQiLCJzdWNjZXNzIjoiVHJ1ZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNaWFtaSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJUYWxsYWhhc3NlZSIsInN1Y2Nlc3MiOiJUcnVlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvaW5pdGlhbGl6ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNToyNTo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9pbml0aWFsaXplZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0yIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjA1OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC4yNX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjM2OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjM1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6Mzg6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0NTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC43NX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjQ3OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0yIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiRmFsc2UiLCJzY29yZSI6NzJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0ODoxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9jb21wbGV0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL2V4dC92YWx1ZSI6IkdhbWUgT3ZlciIsInN1Y2Nlc3MiOiJUcnVlIiwic2NvcmUiOjcyfX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NTk6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9TY3JlZW5zL1NvdW5kTWVudSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL1NjcmVlbiJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vaWQudGluY2FuYXBpLmNvbS92ZXJiL3NraXBwZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDk6MTc6MzdaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZlNwYWluIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ikxpc2JvbiIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ik1pYW1pIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZkZsb3JpZGEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiSmFja3NvbnZpbGxlIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MTM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MTU6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTIiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKdWFuIEdvbWV6In0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuM319LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE3OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC41fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjc1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MzM6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjk1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NDA6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0xIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiRmFsc2UiLCJzY29yZSI6MjJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo1NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKdWFuIEdvbWV6In0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTIiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL2V4dC92YWx1ZSI6IkdhbWUgT3ZlciIsInN1Y2Nlc3MiOiJUcnVlIiwic2NvcmUiOjYwfX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NTU6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IlRydWUiLCJzY29yZSI6NDN9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKdWFuIEdvbWV6In0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9jdXRzY2VuZXMvSW50cm9WaWRlbyIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0N1dHNjZW5lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9WaWRlb1R1dG9yaWFsIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWRyaWQiLCJzdWNjZXNzIjoiVHJ1ZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNaWFtaSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJUYWxsYWhhc3NlZSIsInN1Y2Nlc3MiOiJUcnVlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0K']\n", + " list_of_names = ['xapi-sg-sample-data.json']\n", + " list_of_dates = [1715587109.421]\n", + " 56 zip(list_of_contents, list_of_names, list_of_dates)\n", + " 57 ]\n", + "\n", + "TypeError: load_players_info_from_content() takes 2 positional arguments but 3 were given\n", + "\n" + ] + } + ], + "source": [ + "from dash import Dash, html, dash_table, dcc, callback, Output, Input, State\n", + "import plotly.subplots as subplots\n", + "import fileBrowserAndUploadButtonToLoadProcessStatements\n", + "\n", + "global players_info\n", + "# Initialize the app\n", + "app = Dash(__name__)\n", + "\n", + "# App layout\n", + "app.layout = html.Div([\n", + " html.H1(children='T-Mon'),\n", + " html.Hr(),\n", + " html.H2(children='Select JSON xAPI-SG file to process and see visualisations'),\n", + " dcc.Upload(\n", + " id='upload-data',\n", + " children=html.Div([\n", + " 'Drag and Drop or ',\n", + " html.A('Select Files')\n", + " ]),\n", + " style={\n", + " 'width': '100%',\n", + " 'height': '60px',\n", + " 'lineHeight': '60px',\n", + " 'borderWidth': '1px',\n", + " 'borderStyle': 'dashed',\n", + " 'borderRadius': '5px',\n", + " 'textAlign': 'center',\n", + " 'margin': '10px'\n", + " },\n", + " # Allow multiple files to be uploaded\n", + " multiple=True\n", + " ),\n", + " html.Div(id='output-data-upload'),\n", + " html.Div(id='output-eror', style={'whiteSpace': 'pre-line'}),\n", + " html.Hr(),\n", + " dcc.Tabs(id=\"t-mon-tabs\", value='progress', children=[\n", + " dcc.Tab(label='Progress', value='progress_tab'),\n", + " dcc.Tab(label='Videos', value='video_tab'),\n", + " dcc.Tab(label='Completable', value='completable_tab'),\n", + " dcc.Tab(label='Alternatives', value='alternative_tab'),\n", + " dcc.Tab(label='Interactions', value='interaction_tab'),\n", + " dcc.Tab(label='Accessible', value='interaction_tab'),\n", + " dcc.Tab(label='Menu', value='menu_tab'),\n", + " ]),\n", + " html.H4(children='T-MON, by eUCM research team')\n", + "])\n", + "\n", + "@callback(Output('output-data-upload', 'children'),\n", + " Input('upload-data', 'contents'),\n", + " State('upload-data', 'filename'),\n", + " State('upload-data', 'last_modified'))\n", + "def update_output(list_of_contents, list_of_names, list_of_dates):\n", + " if list_of_contents is not None:\n", + " return [\n", + " fileBrowserAndUploadButtonToLoadProcessStatements.load_players_info_from_content(c, n, d) for c, n, d in\n", + " zip(list_of_contents, list_of_names, list_of_dates)\n", + " ]\n", + " \n", + "if __name__ == '__main__':\n", + " app.run(debug=True, port=8051)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/fileBrowserAndUploadButtonToLoadProcessStatements.ipynb b/fileBrowserAndUploadButtonToLoadProcessStatements.ipynb deleted file mode 100644 index b791bb9..0000000 --- a/fileBrowserAndUploadButtonToLoadProcessStatements.ipynb +++ /dev/null @@ -1,220 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**File explorer** to find the file you want to analyse with **Statement Processor** to generate all data and visualisations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%run ProcessxAPISGStatement.ipynb # notebook to process an xAPI-SG statement\n", - "%run widgets/selectFileWidget.ipynb\n", - "%run widgets/simvaWidget.ipynb\n", - "global location_file, filename,progressbarFile,progressbarTraces, checkboxesPlayersSelected\n", - "progressbarFile = widgets.FloatProgress(value=0.0, min=0.0, max=1.0)\n", - "progressbarTraces = widgets.FloatProgress(value=0.0, min=0.0, max=1.0)\n", - "location_file=\"\"\n", - "filename=\"\"\n", - "\n", - "if storage == 'simva' :\n", - " global simvaWidget\n", - " #*** THE MAIN NOTEBOOK MUST HAVE A a variable of type ipyauth.Auth ***\n", - " simvaWidget = SimvaBrowser(auth=a, storage_url=simva_storage_url, ca_file=simva_ca_file, accept='.json')\n", - "\n", - " def on_file_load(change):\n", - " global location_file, checkboxesPlayersSelected, filename\n", - " players_info.clear()\n", - " # file with xAPI-SG statements\n", - " location_file=simvaWidget.current_path\n", - " basename=location_file.rpartition(simvaWidget.delimiter)[2]\n", - " info = basename.partition('.')\n", - " filename = info[0]\n", - " try:\n", - " start_time = datetime.now()\n", - " ## FILE MUST CONTAIN LIST OF XAPI-SG PROFILE TRACES\n", - " ## (traces separated by commas and enclosed by [])\n", - " statements_str = simvaWidget.get_file_content(location_file)\n", - " nblines=len(statements_str.splitlines())\n", - " trace=0\n", - " for s in statements_str.splitlines():\n", - " progressbarTraces.value=trace/nblines\n", - " trace=trace+1\n", - " statement = json.loads(s)\n", - " process_xapisg_statement(statement, players_info, timeformats)\n", - " end_time = datetime.now()\n", - " except Exception as e:\n", - " with simvaWidget.output:\n", - " print(\"FILE MUST CONTAIN A LIST XAPI-SG PROFILE TRACES (one trace by line)\")\n", - " print(\"This file cannot be analyse by the processor per statement xAPI. Please select an another file.\")\n", - " clear_output(wait=True)\n", - " progressbarTraces.value = 1.0\n", - " with simvaWidget.output:\n", - " print(\"Process per statement OK\")\n", - " print(\"Displaying all vis...\")\n", - " display_checkboxes()\n", - " clear_output(wait=True)\n", - " displayvis(None)\n", - " simvaWidget.buttonRun.on_click(on_file_load)\n", - "else:\n", - " if local:\n", - " global fileBrowser\n", - " #*** !TO BE USED WHEN THE NOTEBOOK IS RUNNED LOCALLY ***\n", - " fileBrowser = FileBrowser(accept='.json')\n", - "\n", - " def on_file_load(change):\n", - " global location_file, checkboxesPlayersSelected, filename\n", - " players_info.clear()\n", - " # file with xAPI-SG statements\n", - " location_file=fileBrowser.path\n", - " basename=os.path.basename(location_file)\n", - " info = os.path.splitext(basename)\n", - " filename = info[0]\n", - " try:\n", - " start_time = datetime.now()\n", - " ## FILE MUST CONTAIN LIST OF XAPI-SG PROFILE TRACES\n", - " ## (traces separated by commas and enclosed by [])\n", - " trace=0\n", - " with open(location_file, 'r', encoding='UTF-8') as file:\n", - " nblines=len(f.splitlines())\n", - " while line := file.readline():\n", - " progressbarTraces.value=trace/nblines\n", - " trace=trace+1\n", - " statement = json.load(line.rstrip())\n", - " process_xapisg_statement(statement, players_info, timeformats)\n", - " end_time = datetime.now()\n", - " except Exception as e:\n", - " with fileBrowser.output:\n", - " print(\"FILE MUST CONTAIN A LIST XAPI-SG PROFILE TRACES (one trace by line)\")\n", - " print(\"This file cannot be analyse by the processor per statement xAPI. Please select an another file.\")\n", - " clear_output(wait=True)\n", - " progressbarTraces.value = 1.0\n", - " with fileBrowser.output:\n", - " print(\"Process per statement OK\")\n", - " print(\"Displaying all vis...\")\n", - " display_checkboxes()\n", - " clear_output(wait=True)\n", - " displayvis(None)\n", - " fileBrowser.buttonRun.on_click(on_file_load)\n", - "\n", - " else:\n", - " global upload_button\n", - " #*** !TO BE USED WHEN THE NOTEBOOK IS HOSTED ON THE WEB ***\n", - " upload_button=widgets.FileUpload(\n", - " description='Upload xAPI data',\n", - " accept='.json', # Accepted file extension e.g. '.txt', '.pdf', 'image/*', 'image/*,.pdf'\n", - " multiple=True, # True to accept multiple files upload, else False\n", - " layout=Layout(width='25%')\n", - " )\n", - " global outTabs,ErrorOut, uploadButtonApp\n", - " outTabs=widgets.Output()\n", - " ErrorOut=widgets.Output()\n", - " uploadButtonApp=VBox([upload_button, progressbarFile, progressbarTraces, ErrorOut, outTabs])\n", - "\n", - " #Observe the file load widget (online version)\n", - " def on_file_upload(change):\n", - " outTabs.clear_output()\n", - " ErrorOut.clear_output()\n", - " upload_button._counter=0\n", - " progressbarFile.value=0.0\n", - " progressbarTraces.value=0.0\n", - " filename=\"\"\n", - " number_of_files = len(upload_button.value) # number of json files selected\n", - " players_info.clear()\n", - " errors=[]\n", - " with ErrorOut:\n", - " for file_index in range(0,number_of_files):\n", - " progressbarFile.value = file_index/number_of_files\n", - " location_file = upload_button.value[file_index].name #get the file name from the embedded metadata dict\n", - " file = '-'.join(location_file.split(\".\")[:-1])\n", - " f = upload_button.value[file_index].content.tobytes().decode(\"utf-8\") #extract data byte string and convert to str\n", - " filename += file + \"-\"\n", - " nblines = len(f.splitlines())\n", - " start_time = datetime.now()\n", - " ## FILE MUST CONTAIN LIST OF XAPI-SG PROFILE TRACES\n", - " ## (traces separated by commas and enclosed by [])\n", - " trace=0\n", - " #print(\"File \", location_file, \"(\", file_index , \"/\", number_of_files, \") : Processing \", nblines,\" traces\")\n", - " for s in f.splitlines() :\n", - " progressbarTraces.value=trace/nblines\n", - " trace=trace+1\n", - " try:\n", - " statement = json.loads(s)\n", - " process_xapisg_statement(statement, players_info, timeformats)\n", - " except Exception as e:\n", - " errors.append(s)\n", - " end_time = datetime.now()\n", - " progressbarFile.value = 1.0\n", - " progressbarTraces.value = 1.0\n", - " nbError=len(errors)\n", - " print(\"Nb error : \", nbError, \" | Nb total lines:\", nblines)\n", - " if nbError == nblines:\n", - " print(\"FILE MUST CONTAIN A LIST XAPI-SG PROFILE TRACES (one trace by line)\")\n", - " print(\"This file\" + location_file + \" cannot be analyse by the processor per statement xAPI. Please select an another file.\")\n", - " players_info.clear()\n", - " with outTabs:\n", - " print(\"Process per statement OK\")\n", - " print(\"Displaying all vis...\")\n", - " clear_output(wait=True)\n", - " display_checkboxes()\n", - " displayvis(None)\n", - " upload_button._counter=0\n", - " upload_button.observe(on_file_upload, names='value')\n", - "\n", - "def displayvis(change):\n", - " if storage == 'simva' :\n", - " with simvaWidget.output:\n", - " displayAllVisualisations()\n", - " simvaWidget.output.clear_output(wait=True)\n", - " else:\n", - " if local:\n", - " with fileBrowser.output:\n", - " displayAllVisualisations()\n", - " fileBrowser.output.clear_output(wait=True)\n", - " else:\n", - " if not(players_info == {}) :\n", - " ErrorOut.clear_output()\n", - " with outTabs:\n", - " displayAllVisualisations()\n", - " outTabs.clear_output(wait=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/fileBrowserAndUploadButtonToLoadProcessStatements.py b/fileBrowserAndUploadButtonToLoadProcessStatements.py new file mode 100644 index 0000000..4f2b475 --- /dev/null +++ b/fileBrowserAndUploadButtonToLoadProcessStatements.py @@ -0,0 +1,122 @@ +import datetime +import json +# only for demo +import sys +import os +import io +import base64 +from dataclasses import dataclass +import traceback +import ProcessxAPISGStatement +from dash import html +# +# +# +@dataclass +class Progress: + """Fools load_from_string, keeps track of progress.""" + value: float + +# +# fileBrowserAndUploadButtonToLoadProcessStatements.ipynb +# + +def is_json_and_not_list(str): + try: + return not isinstance(json.loads(str), list) + except Exception as e: + return False + +# +# Loads either JSON-statement-per-line or JSON-array-of-statements from a str +# Updates progress in progress by calling progress.value from 0.0 to 1.0 (=finished) +# err_output & info_output can be either +# None (= no output), +# a list (= ouput strings get appended), or +# an object o where with o.output: print() is valid +# +def load_from_string(str, progress, info_output, err_output, players_info, timeformats): + def log(target, o_str): + if type(target) is None: + pass + elif type(target) is list: + target.append(o_str) + else: + with target.output: + print(o_str) + total=0 + count=0 + try: + start_time = datetime.datetime.now() + if is_json_and_not_list(str.partition('\n')[0]): + total=len(str.splitlines()) + log(info_output, f"... 1st line is valid JSON; interpreting as one-statement-per-line ({total} statement(s))") + # 1st line is well-formed json, and not json list of statements; assume 1-statement-per-line + for s in str.splitlines(): + progress.value=count/total + count+=1 + ProcessxAPISGStatement.process_xapisg_statement(json.loads(s), players_info, timeformats) + else: + log(info_output, "... interpreting as statement-array") + # attempt to process all of it as a single JSON document + statements = json.loads(str) + total = len(statements) + log(info_output, f"... parsed correctly as json array, total statements = {total}") + for s in statements: + progress.value=count/total + count+=1 + ProcessxAPISGStatement.process_xapisg_statement(s, players_info, timeformats) + except Exception as e: + log(err_output, + f"ERROR loading at line/statement {count}/{total}: {e}\n" + f"Full error:\n~~~\n{''.join(traceback.format_exception(e))}\n~~~\n" + f"File must contain EITHER 1 statement (in JSON) per line, or be a well-formed JSON of statements. Please select another file.") + return e + progress.value=1.0 + log(info_output, f"... processed {count}/{total} statement(s) in {datetime.datetime.now() - start_time}. Displaying visualizations ...") + return players_info + +timeformats=['%Y-%m-%dT%H:%M:%SZ','%Y-%m-%dT%H:%M:%S.%fZ'] +players_info = {} + +def load_players_info_from_file(file): + with open(file, encoding="utf-8") as f: + str = f.read() + progress = Progress(0) + out, err = [], [] + load_from_string(str, progress, out, err, players_info, timeformats) + print(f"Info log ({len(out)} lines):\n", '\n'.join(out)) + if len(err) > 0: + print(f"ERRORS FOUND ({len(err)} lines):\n", '\n'.join(err)) + sys.exit(-1) + return players_info + +def load_players_info_from_content(filecontent, filename, date): + content_type, content_string = filecontent.split(',') + decoded = base64.b64decode(content_string) + progress = Progress(0) + out, err = [], [] + try: + if 'json' in filename: + load_from_string(decoded, progress, out, err, players_info, timeformats) + except Exception as e: + print(e) + return html.Div( + html.Div([ + 'There was an error processing this file.' + ]), + html.Div(out), + html.Div(err), + ]) + return html.Div([ + html.H5(filename), + html.H6(datetime.datetime.fromtimestamp(date)), + html.Div(out), + html.Hr(), # horizontal line + # For debugging, display the raw contents provided by the web browser + html.Div('Raw Content'), + html.Pre(filecontent[0:200] + '...', style={ + 'whiteSpace': 'pre-wrap', + 'wordBreak': 'break-all' + }) + ]) \ No newline at end of file diff --git a/ipyauth-keycloak-demo.env b/ipyauth-keycloak-demo.env deleted file mode 100644 index 00b2d19..0000000 --- a/ipyauth-keycloak-demo.env +++ /dev/null @@ -1,5 +0,0 @@ -base_url=https://sso.simva.e-ucm.es -realm=simva -response_type=id_token token -client_id=jupyter -redirect_uri=http://localhost:8888/callback/ diff --git a/requirements.txt b/requirements.txt index 894bcdc..6b4b95c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ ipympl nbformat requests boto3 -jwt \ No newline at end of file +jwt +plotly +dash \ No newline at end of file From b4f266770cf184e9ad33055d0c39dfc4db29c4b2 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER <62715763+jujusb@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:29:28 +0200 Subject: [PATCH 2/4] fix tmon using plotly and dash adding more visualisations in demo fix gitignore adding publish new version to docker hub when pushing in master fix keycloak ssl client Update Dockerfile fix message fix logout fix case logout Keycloak fix update browser when empty combined with upload button Update KeycloakClient.py fix keycloak client ssl cert file remove unecesary code fix redirect uri fix logs fix referer uri fix fix fix keycloak host fix redirect to account fix proxy remove policy role attribute from keycloak not needed anymore fix syntax error fix syntax error fix syntax error fix using get presigned url update default host and default port + adding dockerfile fix adding test in tmon path fix simva browser using //ouput//tracefilename in minio fix access to traces files updated simva browser to only take in account study and activty data from simva api connected via the user Simva browser updated fix display name Adding study and activity name to folders in simva browser fix template client secret fix some url + one duplicated function name fix error message fix simva browser to display file not exists or don't have access adding video seen skipped + filter object id in function of the tab adding object id as multiselector fix tmon upload to let current url fix update value and tab from url in new callback function fix url location + add actor.name in url fix url location adding url redirect adding url to go directly to the good page of dashboard fix https vs http adding native filter and sort to data table remove unnecessary importq fix import location fix backgroundColor adding title of game in visualisation fix variables fix function name use library for the function for the selector Update xAPISGPlayersProgress.py Update LoadProcessStatements.py fix accountpage adding display no data to display vis fix module names fix typo fix typo fix typo file name fix t-mon structure fix xapidata shared across module + fix typo + adding homePage to T-Mon after loading data into T-Mon try fixing xapiData as global variable Adding in separate libraries fix player progress Adding first visualisation in TMonApp fix demo remove helper function using matplotlib libraries remove debug for browser fix process xapi statements fix in only one part fix T-Mon app fix self.accepted_activities remove url_base_pathname for tmon app using upload fix keycloak auth adding get list of activities from simva api to filter minio folder rm output of notebook adding display of table of xapi data removing import unnecesary fxi simva widgets Update auth_keycloak_dash.ipynb fix simva widget fix simva widget adding simva widget Update auth_keycloak_dash.ipynb auth to Keycloak integrated in dashboard fix some demo graphs adding keycloak auth via flask_oidc score by completable update fix extensions + object id for progres remove custom function unused fix cutscene Fix some progress data that where before initialized fix sample data for cutscenes t-mon table xapi data working and displaying adding xapi table upgrading tmon dashboard add app.py fix app.py .gitignore remove output --- .dockerignore | 5 + .github/workflows/docker-publish.yml | 96 + .gitignore | 6 +- Dockerfile | 27 + ...sStatements.py => LoadProcessStatements.py | 104 +- ProcessxAPISGStatement.py | 226 -- SimvaBrowser/KeycloakClient.py | 126 + SimvaBrowser/MinioAdminBrowser.py | 116 + SimvaBrowser/MinioBrowser.py | 156 + SimvaBrowser/SimvaAPIBrowser.py | 120 + SimvaBrowser/SimvaBrowser.py | 253 ++ SimvaBrowser/__init__.py | 0 T-Mon.ipynb | 126 - TMonApp.py | 16 + TMonSimvaApp.py | 21 + TMonWidgets/MultiSelector.py | 19 + TMonWidgets/SimvaBrowserWidget.py | 322 ++ TMonWidgets/TMonWidget.py | 272 ++ TMonWidgets/UploadWidget.py | 61 + TMonWidgets/__init__.py | 1 + __init__.py | 0 client_secrets_example.json | 21 + demo.ipynb | 3453 +---------------- requirements.txt | 5 +- vis/__init__.py | 0 vis/helpersFunctions/HeatMap.ipynb | 132 - vis/helpersFunctions/MultiBarMultiColor.ipynb | 80 - vis/helpersFunctions/MultiBarSeparated.ipynb | 180 - ...OrderGraphValuesAndSelectDataByXaxis.ipynb | 370 -- vis/helpersFunctions/TimeLineChart.ipynb | 57 - vis/helpersFunctions/barCharts.ipynb | 406 -- .../bubbleChartFunctionTime.ipynb | 100 - vis/helpersFunctions/clearFigMatplotlib.ipynb | 408 -- vis/helpersFunctions/getterObjectsList.ipynb | 152 - vis/xAPISG-PlayersProgress.ipynb | 165 - vis/xAPISG-VideosSeenSkipped.ipynb | 283 -- vis/xAPISG-noDataToFillVisualization.ipynb | 62 - vis/xAPISGPlayersProgress.py | 109 + vis/xAPISGVideosSeenSkipped.py | 28 + vis/xAPISGnoDataToFillVisualization.py | 29 + .../multipleSelectorCheckboxesWidget.ipynb | 184 - widgets/selectFileWidget.ipynb | 122 - widgets/simvaWidget.ipynb | 177 - xapi-sg-sample-data-array.json | 370 +- xapi-sg-sample-data.json | 168 +- 45 files changed, 2310 insertions(+), 6824 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish.yml create mode 100644 Dockerfile rename fileBrowserAndUploadButtonToLoadProcessStatements.py => LoadProcessStatements.py (51%) delete mode 100644 ProcessxAPISGStatement.py create mode 100644 SimvaBrowser/KeycloakClient.py create mode 100644 SimvaBrowser/MinioAdminBrowser.py create mode 100644 SimvaBrowser/MinioBrowser.py create mode 100644 SimvaBrowser/SimvaAPIBrowser.py create mode 100644 SimvaBrowser/SimvaBrowser.py create mode 100644 SimvaBrowser/__init__.py delete mode 100644 T-Mon.ipynb create mode 100644 TMonApp.py create mode 100644 TMonSimvaApp.py create mode 100644 TMonWidgets/MultiSelector.py create mode 100644 TMonWidgets/SimvaBrowserWidget.py create mode 100644 TMonWidgets/TMonWidget.py create mode 100644 TMonWidgets/UploadWidget.py create mode 100644 TMonWidgets/__init__.py create mode 100644 __init__.py create mode 100644 client_secrets_example.json create mode 100644 vis/__init__.py delete mode 100644 vis/helpersFunctions/HeatMap.ipynb delete mode 100644 vis/helpersFunctions/MultiBarMultiColor.ipynb delete mode 100644 vis/helpersFunctions/MultiBarSeparated.ipynb delete mode 100644 vis/helpersFunctions/OrderGraphValuesAndSelectDataByXaxis.ipynb delete mode 100644 vis/helpersFunctions/TimeLineChart.ipynb delete mode 100644 vis/helpersFunctions/barCharts.ipynb delete mode 100644 vis/helpersFunctions/bubbleChartFunctionTime.ipynb delete mode 100644 vis/helpersFunctions/clearFigMatplotlib.ipynb delete mode 100644 vis/helpersFunctions/getterObjectsList.ipynb delete mode 100644 vis/xAPISG-PlayersProgress.ipynb delete mode 100644 vis/xAPISG-VideosSeenSkipped.ipynb delete mode 100644 vis/xAPISG-noDataToFillVisualization.ipynb create mode 100644 vis/xAPISGPlayersProgress.py create mode 100644 vis/xAPISGVideosSeenSkipped.py create mode 100644 vis/xAPISGnoDataToFillVisualization.py delete mode 100644 widgets/multipleSelectorCheckboxesWidget.ipynb delete mode 100644 widgets/selectFileWidget.ipynb delete mode 100644 widgets/simvaWidget.ipynb diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd696b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +docs +.github +*.ipynb +Dockerfile +.dockerignore \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..04b9751 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,96 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + push: + branches: [ "master" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "master" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: docker.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 62c8935..a161af9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.idea/ \ No newline at end of file +.idea/ +.venv/ +__pycache__ +client_secrets.json +*.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..76f84e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Use an official Python runtime as a parent image +FROM python:3.9-slim + +# Install ca-certificates and update them +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates && \ + update-ca-certificates && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY . /app + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Make port 8050 available to the world outside this container +EXPOSE 8050 + +# Define environment variable +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Run app.py when the container launches +CMD ["python", "TMonSimvaApp.py"] \ No newline at end of file diff --git a/fileBrowserAndUploadButtonToLoadProcessStatements.py b/LoadProcessStatements.py similarity index 51% rename from fileBrowserAndUploadButtonToLoadProcessStatements.py rename to LoadProcessStatements.py index 4f2b475..8606ffa 100644 --- a/fileBrowserAndUploadButtonToLoadProcessStatements.py +++ b/LoadProcessStatements.py @@ -1,17 +1,11 @@ -import datetime +from datetime import datetime import json -# only for demo +import pandas as pd import sys -import os -import io import base64 from dataclasses import dataclass import traceback -import ProcessxAPISGStatement -from dash import html -# -# -# + @dataclass class Progress: """Fools load_from_string, keeps track of progress.""" @@ -27,6 +21,15 @@ def is_json_and_not_list(str): except Exception as e: return False +def log(target, o_str): + if type(target) is None: + pass + elif type(target) is list: + target.append(o_str) + else: + with target.output: + print(o_str) + # # Loads either JSON-statement-per-line or JSON-array-of-statements from a str # Updates progress in progress by calling progress.value from 0.0 to 1.0 (=finished) @@ -35,27 +38,23 @@ def is_json_and_not_list(str): # a list (= ouput strings get appended), or # an object o where with o.output: print() is valid # -def load_from_string(str, progress, info_output, err_output, players_info, timeformats): - def log(target, o_str): - if type(target) is None: - pass - elif type(target) is list: - target.append(o_str) - else: - with target.output: - print(o_str) +# Callback to update progress periodically +def load_from_string(str, xapiData, info_output, err_output): + progress = Progress(0) total=0 count=0 try: - start_time = datetime.datetime.now() + start_time = datetime.now() if is_json_and_not_list(str.partition('\n')[0]): total=len(str.splitlines()) log(info_output, f"... 1st line is valid JSON; interpreting as one-statement-per-line ({total} statement(s))") # 1st line is well-formed json, and not json list of statements; assume 1-statement-per-line - for s in str.splitlines(): + statements=str.splitlines() + for statement in statements: + s=json.loads(statement) progress.value=count/total count+=1 - ProcessxAPISGStatement.process_xapisg_statement(json.loads(s), players_info, timeformats) + processxapisgdata(s, xapiData) else: log(info_output, "... interpreting as statement-array") # attempt to process all of it as a single JSON document @@ -65,7 +64,7 @@ def log(target, o_str): for s in statements: progress.value=count/total count+=1 - ProcessxAPISGStatement.process_xapisg_statement(s, players_info, timeformats) + processxapisgdata(s, xapiData) except Exception as e: log(err_output, f"ERROR loading at line/statement {count}/{total}: {e}\n" @@ -73,50 +72,31 @@ def log(target, o_str): f"File must contain EITHER 1 statement (in JSON) per line, or be a well-formed JSON of statements. Please select another file.") return e progress.value=1.0 - log(info_output, f"... processed {count}/{total} statement(s) in {datetime.datetime.now() - start_time}. Displaying visualizations ...") - return players_info + log(info_output, f"... processed {count}/{total} statement(s) in {datetime.now() - start_time}. Displaying visualizations ...") -timeformats=['%Y-%m-%dT%H:%M:%SZ','%Y-%m-%dT%H:%M:%S.%fZ'] -players_info = {} - -def load_players_info_from_file(file): +def load_players_info_from_file(file, xapiData, out, err): + log(out, f"{file}") with open(file, encoding="utf-8") as f: str = f.read() - progress = Progress(0) - out, err = [], [] - load_from_string(str, progress, out, err, players_info, timeformats) - print(f"Info log ({len(out)} lines):\n", '\n'.join(out)) + load_from_string(str, xapiData, out, err) + print(f"Info log ({len(out)} lines):\n" + "\n".join(out)) if len(err) > 0: - print(f"ERRORS FOUND ({len(err)} lines):\n", '\n'.join(err)) + print(f"ERRORS FOUND ({len(err)} lines):\n" + "\n".join(err)) sys.exit(-1) - return players_info -def load_players_info_from_content(filecontent, filename, date): +def load_players_info_from_uploaded_content(filecontent, filename, xapiData, out, err): + log(out, f"{filename}") content_type, content_string = filecontent.split(',') - decoded = base64.b64decode(content_string) - progress = Progress(0) - out, err = [], [] - try: - if 'json' in filename: - load_from_string(decoded, progress, out, err, players_info, timeformats) - except Exception as e: - print(e) - return html.Div( - html.Div([ - 'There was an error processing this file.' - ]), - html.Div(out), - html.Div(err), - ]) - return html.Div([ - html.H5(filename), - html.H6(datetime.datetime.fromtimestamp(date)), - html.Div(out), - html.Hr(), # horizontal line - # For debugging, display the raw contents provided by the web browser - html.Div('Raw Content'), - html.Pre(filecontent[0:200] + '...', style={ - 'whiteSpace': 'pre-wrap', - 'wordBreak': 'break-all' - }) - ]) \ No newline at end of file + decoded = base64.b64decode(content_string).decode('utf-8') + load_from_string(decoded, xapiData, out, err) + +def load_players_info_from_content(filecontent, filename, xapiData, out, err): + log(out, f"{filename}") + decoded = filecontent.decode('utf-8') + load_from_string(decoded, xapiData, out, err) + +def processxapisgdata(statement, xapiData): + # Extract the `id` from the first category object + if "context" in statement and "contextActivities" in statement["context"] and "category" in statement["context"]["contextActivities"] and len(statement["context"]["contextActivities"]["category"]) > 0: + statement["context"]["contextActivities"]["category"] = statement["context"]["contextActivities"]["category"][0]["id"] + xapiData.append(statement) \ No newline at end of file diff --git a/ProcessxAPISGStatement.py b/ProcessxAPISGStatement.py deleted file mode 100644 index 2325f91..0000000 --- a/ProcessxAPISGStatement.py +++ /dev/null @@ -1,226 +0,0 @@ -# imports from actually useful code -import numpy as np -import copy -import math -import datetime - -# -# ProcessxAPISGStatement.ipynb -# -#***timestampTotimedate* function** to tranform the timestamp in datetime -#Inputs: -#* timestamp : the time in a string -#* timeformats : an array of timeformat witch match the timestamp with -# -#Output: -#* return t : dateTime -#* raise TimeFormatError : in case of the timestamp not in timeformat array -def timestampTotimedate(timestamp, timeformats): - t=None - for timeformat in timeformats: - try: - t = datetime.datetime.strptime(timestamp, timeformat) - except ValueError: - pass - if t==None: - str="TimeFormatError : This timestamp don't match with formats in " - for format in timeformats: - str+=format+" " - raise TimeFormatError(timestamp, str) - else: - return t - -class Error(Exception): - """Base class for exceptions in this module.""" - pass - -class TimeFormatError(Error): - """Exception raised for errors in timeformat. - - Attributes: - expression -- input expression in which the error occurred - message -- explanation of the error - """ - - def __init__(self, expression, message): - self.expression = expression - self.message = message - -# template with default information for each player -template_player_info = { - "game_started": False, "game_completed": False, - "interactions":{}, # dict of interactions - "game_progress_per_time": [], # list of pairs (game progress, timestamp) - "completables_scores": {}, # dict completable : last score - "completables_progress": {}, # list of pairs completable : last progress - "completables_times": {}, # dict completable: (start, end) - "alternatives": {}, # dict alternative: list of pairs (response, correct (T/F)) - "action_type_interaction":{}, # dict of action type interactions - "accessible":{}, # dict of accessible - "videos_seen": [], # list of videos seen (accessed) by player - "videos_skipped": [], # list of videos skipped by player - "selected_menus":{} # dict of menus and response selected -} - -#***process_xapisg_statement* function** receives an xAPI-SG statement and updates the dictionary of players information -# -# -#Inputs: -#* data : xAPI-SG statement -#* players_info: dictionary with players info -#* timeformats: list of timeformats for timestamp -def process_xapisg_statement(data, players_info, timeformats): - # available keys in statement - keys = data.keys() - - ## extracting fields from xAPI-SG statement - # actor field - if "actor" in keys: - if "name" in data["actor"].keys(): - actor_name = data["actor"]["name"] - - if actor_name not in players_info.keys(): - players_info[actor_name] = copy.deepcopy(template_player_info) - - player_info = players_info[actor_name] - - # verb field - if "verb" in keys: - if "id" in data["verb"].keys(): - verb_id = data["verb"]["id"] - # process verb field - verb_xapi = np.array(verb_id.split("/"))[-1] - - # object field - if "object" in keys: - if "id" in data["object"].keys(): - object_id = data["object"]["id"] - # process object id field - object_id_name = np.array(object_id.split("/"))[-1] - if "definition" in data["object"].keys() and "type" in data["object"]["definition"].keys(): - object_type = data["object"]["definition"]["type"] - # process object type field - object_type_xapi = np.array(object_type.split("/"))[-1] - action_type=None - # result field - if "result" in keys: - if "extensions" in data["result"].keys(): - if "response" in data["result"].keys(): - result_response = data["result"]["response"] - res = data["result"]["extensions"] - else: - res = data["result"] - if "success" in res.keys(): - result_success = res["success"] - if "response" in res.keys(): - result_response = res["response"] - if "progress" in res.keys(): - result_progress = res["progress"] - elif "https://w3id.org/xapi/seriousgames/extensions/progress" in res.keys(): - result_progress = res["https://w3id.org/xapi/seriousgames/extensions/progress"] - if "score" in res.keys(): - if(isinstance(res["score"],dict)): - result_score = res["score"]["raw"] - else: - result_score = res["score"] - if "action_type" in res.keys(): - action_type=res["action_type"] - - # timestamp field - if "timestamp" in keys: - timestamp = data["timestamp"] - try: - t=timestampTotimedate(timestamp, timeformats) - except TimeFormatError as e: - print(e) - - ## update values - try: - # initialized traces - if verb_xapi.lower()=="initialized": - if object_type_xapi.lower()=="serious-game": - player_info["game_started"] = True - if timestamp: player_info["game_progress_per_time"].append([0,t]) - - if timestamp: player_info["completables_times"][object_id_name] = (t, 0) # (start, end) times - - # completed traces - elif verb_xapi.lower()=="completed": - if object_type_xapi.lower()=="serious-game": - player_info["game_completed"] = True - if timestamp: player_info["game_progress_per_time"].append([1,t]) - - if timestamp and object_id_name in player_info["completables_times"].keys(): - prev = player_info["completables_times"][object_id_name][0] - player_info["completables_times"][object_id_name] = (prev, t) # update end time - if object_id_name and timestamp and result_score: - player_info["completables_scores"][object_id_name] = result_score - - # progressed traces - elif verb_xapi.lower()=="progressed": - if object_type_xapi.lower()=="serious-game" and timestamp and result_progress: - player_info["game_progress_per_time"].append([result_progress,t]) - if result_progress==1: - player_info['game_completed'] = True - - if verb_xapi.lower()=="progressed" and object_id_name and result_progress: - if not object_id_name in player_info["completables_progress"].keys(): - player_info["completables_progress"][object_id_name]=[] - player_info["completables_progress"][object_id_name].append([result_progress,t]) - - # interacted and used traces - elif verb_xapi.lower()=="interacted" or verb_xapi.lower()=="used": - if action_type!=None: - if not object_type_xapi in player_info["action_type_interaction"].keys(): - player_info["action_type_interaction"][object_type_xapi]={} - - if not object_id_name in player_info["action_type_interaction"][object_type_xapi].keys(): - player_info["action_type_interaction"][object_type_xapi][object_id_name]={} - - if not action_type in player_info["action_type_interaction"][object_type_xapi][object_id_name].keys(): - player_info["action_type_interaction"][object_type_xapi][object_id_name][action_type]=[] - - player_info["action_type_interaction"][object_type_xapi][object_id_name][action_type].append(t) - - else: - if not object_type_xapi in player_info["interactions"].keys(): - player_info["interactions"][object_type_xapi]={} - - if not object_id_name in player_info["interactions"][object_type_xapi].keys(): - player_info["interactions"][object_type_xapi][object_id_name]=[] - - player_info["interactions"][object_type_xapi][object_id_name].append(t) - - # selected traces - elif verb_xapi.lower()=="selected": - if object_type_xapi.lower()=="alternative": - if object_id_name and result_response and result_success is not None: # success = false is valid! - if object_id_name in player_info["alternatives"].keys(): - player_info["alternatives"][object_id_name].append((result_response, result_success)) - else: - player_info["alternatives"][object_id_name] = [(result_response, result_success)] - - elif object_type_xapi.lower()=="menu": - if result_response: - if not object_id_name in player_info["selected_menus"].keys(): - player_info["selected_menus"][object_id_name]={} - if not result_response in player_info["selected_menus"][object_id_name].keys(): - player_info["selected_menus"][object_id_name][result_response]=[] - if timestamp: - t=timestampTotimedate(timestamp, timeformats) - player_info["selected_menus"][object_id_name][result_response].append(t) - # accessed traces - elif verb_xapi.lower()=="accessed": - if object_type_xapi.lower()=="cutscene" and object_id_name: - player_info["videos_seen"].append(object_id_name) - elif object_type_xapi.lower()=="accessible" and object_id_name: - if not object_id_name in player_info["accessible"].keys(): - player_info["accessible"][object_id_name]=[] - t=timestampTotimedate(timestamp, timeformats) - player_info["accessible"][object_id_name].append(t) - # skipped traces - elif verb_xapi.lower()=="skipped": - if object_type_xapi.lower()=="cutscene" and object_id_name: - player_info["videos_skipped"].append(object_id_name) - except NameError: - pass#%% md diff --git a/SimvaBrowser/KeycloakClient.py b/SimvaBrowser/KeycloakClient.py new file mode 100644 index 0000000..679951f --- /dev/null +++ b/SimvaBrowser/KeycloakClient.py @@ -0,0 +1,126 @@ +import json +import logging +import os +from flask import Flask, redirect, url_for, session, request +from flask_oidc import OpenIDConnect +from werkzeug.middleware.proxy_fix import ProxyFix + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +class KeycloakClient: + def __init__(self, client_secret_file="client_secrets.json", homepage = True): + self.flaskServer = Flask(__name__) + self.flaskServer.wsgi_app = ProxyFix(self.flaskServer.wsgi_app, x_for=1, x_host=1, x_port=1, x_proto=1, x_prefix=1) + basedir = os.path.abspath(f"{os.path.dirname(__file__)}/../") + self.flaskServer.config.update({ + 'SECRET_KEY': 'SomethingNotEntirelySecret', + 'TESTING': True, + 'DEBUG': True, + 'OIDC_CLIENT_SECRETS': os.path.join(basedir, client_secret_file), + 'OIDC_ID_TOKEN_COOKIE_SECURE': True, + 'OIDC_USER_INFO_ENABLED': True, + 'OIDC_OPENID_REALM': 'simva', + 'OIDC_SCOPES': ['openid', 'email', 'profile', 'roles', 'web-origins'], + 'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post' + }) + self.oidc = OpenIDConnect(self.flaskServer) + issuer_url = self.oidc.client_secrets.get('issuer') + client_id = self.oidc.client_secrets.get('client_id') + host=self.oidc.client_secrets.get('host') + port=self.oidc.client_secrets.get('port') + #ssl_cert_file=self.oidc.client_secrets.get('ssl_cert_file') + #if(ssl_cert_file): + # self.flaskServer.config.update({ + # 'SSL_CERT_FILE': ssl_cert_file, + # 'SSL_VERIFY': True + # }) + #else: + # self.flaskServer.config.update({ + # 'SSL_VERIFY': False + # }) + refereruri=f"https://{host}" if self.oidc.client_secrets.get('secure') == "True" else f"http://{host}:{port}" + print(refereruri) + self.accountpage=f"{issuer_url}/account?referrer={client_id}&referrer_uri={refereruri}" + self.homepage="/" + if homepage: + @self.flaskServer.route(self.homepage) + def home(): + print(f"Accessing route /") + if self.oidc.user_loggedin: + user_info = session.get('oidc_auth_profile', {}) + user_id = user_info.get('sub') + email = user_info.get('email') + preferred_username = user_info.get('preferred_username') + redirect_uri = url_for('home', _external=True) + return (f"""Hello {preferred_username}, your email is {email} and your user_id is {user_id}! + """) + else: + return f'Welcome anonymous, Log in' + self.login="/login" + @self.flaskServer.route(self.login) + @self.oidc.require_login + def login(): + if homepage: + redirect_uri = f"{url_for('home', _external=True)}" + else: + redirect_uri = request.base_url.replace(self.login,self.homepage) + print(f"Accessing route /login") + return redirect(redirect_uri) + + self.apiroute='/api' + @self.flaskServer.route(self.apiroute, methods=['POST']) + @self.oidc.accept_token(require_token=True, scopes_required=['openid']) + def api(): + """OAuth 2.0 protected API endpoint accessible via AccessToken""" + print(f"Accessing route /api") + user_info = session.get('oidc_auth_profile', {}) + preferred_username = user_info.get('preferred_username') + return json.dumps({'hello': f"Welcome {preferred_username}"}) + + self.logoutroute='/logoutkeycloak' + @self.flaskServer.route(self.logoutroute) + def logoutkeycloak(): + """Performs local logout by removing the session cookie.""" + print(f"Accessing route /logoutkeycloak") + if homepage: + redirect_uri = f"{url_for('home', _external=True)}" + else: + redirect_uri = request.base_url.replace(self.logoutroute,self.homepage) + + issuer_url = self.oidc.client_secrets.get('issuer') + logger.debug(f'Received logout request. Redirect URI: {redirect_uri}, Issuer URL: {issuer_url}') + if self.oidc.user_loggedin: + # Get the ID token from the session (assuming it's stored in 'id_token' key) + auth_token = session.get('oidc_auth_token', {}) + logger.debug(auth_token) + id_token = auth_token.get('id_token') + logger.debug(f'User logged in. Id token: {id_token}') + + # Log out locally + self.oidc.logout() + logger.debug('Local logout successful.') + + # Clear the session + session.clear() + logger.debug('Session cleared.') + + # Construct the Keycloak logout URL + keycloak_base_url = f'{issuer_url}/protocol/openid-connect/logout' + keycloak_logout_url = f'{keycloak_base_url}?id_token_hint={id_token}&post_logout_redirect_uri={redirect_uri}' + logger.debug(f'Keycloak logout URL: {keycloak_logout_url}') + return redirect(keycloak_logout_url) + else: + logger.debug('User not logged in. Redirecting to home.') + return redirect(redirect_uri) + +if __name__ == '__main__': + import requests + # Ensure the certificate is trusted + requests.packages.urllib3.disable_warnings() # Optional, suppress SSL warnings + #os.environ['REQUESTS_CA_BUNDLE'] = '/usr/local/share/ca-certificates/internal-CA.crt' + flask = KeycloakClient() + flask.flaskServer.run(debug=True, port=5000) \ No newline at end of file diff --git a/SimvaBrowser/MinioAdminBrowser.py b/SimvaBrowser/MinioAdminBrowser.py new file mode 100644 index 0000000..da394d4 --- /dev/null +++ b/SimvaBrowser/MinioAdminBrowser.py @@ -0,0 +1,116 @@ +import xml.etree.ElementTree as ET +import boto3 +import boto3.session +from botocore.exceptions import ClientError +import os +import json + +class MinioBrowser: + def __init__(self, accept='.json', ca_file=None, delimiter='/', client_secret_file="client_secrets.json"): + basedir = os.path.abspath(f"{os.path.dirname(__file__)}/../") + self.secret_file=self._load_secret_file(os.path.join(basedir, client_secret_file)) + self.accept = accept + self.ca_file = ca_file + self.storage_url = self.secret_file.get("minio").get("storage_url") + self.bucket_name = self.secret_file.get("minio").get("bucket_name") + self.access_key_id = self.secret_file.get("minio").get("access_key_id") + self.secret_access_key = self.secret_file.get("minio").get("secret_access_key") + self.traces_folder = self.secret_file.get("minio").get("output_folder") + self.delimiter = delimiter + self.base_path = self.traces_folder + self.delimiter + self.current_path = self.base_path + self.current_level = 0 + self._update_files() + + def _load_secret_file(self, file_path): + with open(file_path, 'r') as file: + secret_data = json.load(file) + return secret_data + + def _s3_client(self): + return boto3.client( + 's3', + endpoint_url=self.storage_url, + aws_access_key_id=self.access_key_id, + aws_secret_access_key=self.secret_access_key, + verify=self.ca_file + ) + + def _list_files(self, path): + try: + s3_client = self._s3_client() + folder = s3_client.list_objects_v2(Bucket=self.bucket_name, + Prefix=path, + Delimiter=self.delimiter) + files = [] + contents = folder.get('Contents') + print(f"Folder : {folder}") + if contents: + for o in contents: + files.append(o.get('Key')[len(self.current_path):]) + return files + except ClientError as e: + print(f"An error occurred: {e}") + if e.response['Error']['Code'] == 'AccessDenied': + print("Access denied. Check your IAM policies and bucket policies.") + return [] + + def _list_folders(self, path): + try: + s3_client = self._s3_client() + folder = s3_client.list_objects_v2(Bucket=self.bucket_name, + Prefix=path, + Delimiter=self.delimiter) + folders = [] + contents = folder.get('CommonPrefixes') + if contents: + for o in contents: + folders.append(o.get('Prefix')[len(self.current_path):]) + folders=[{"id":dir_id,"name": dir_id} for dir_id in folders] + return folders + except ClientError as e: + print(f"An error occurred: {e}") + if e.response['Error']['Code'] == 'AccessDenied': + print("Access denied. Check your IAM policies and bucket policies.") + return [] + + def get_file_content(self, path): + try: + s3_client = self._s3_client() + file = s3_client.get_object(Bucket=self.bucket_name, Key=path) + return file['Body'].read() + except ClientError as e: + print(f"An error occurred: {e}") + if e.response['Error']['Code'] == 'AccessDenied': + print("Access denied. Check your IAM policies and bucket policies.") + return [] + + def _isdir(self, path): + return path.endswith(self.delimiter) + + def _update_files(self): + self.files = [] + self.dirs = [] + self.current_level=len(self.current_path.replace(self.base_path, "").split("/")) + if self._isdir(self.current_path): + self.dirs = self._list_folders(self.current_path) + self.files = self._list_files(self.current_path) + + def file_exists(self, path): + """ + Check if a file exists in the S3 bucket at the given path. + + :param path: The path of the file in the S3 bucket. + :return: True if the file exists, False otherwise. + """ + try: + s3_client = self._s3_client() + file = s3_client.get_object(Bucket=self.bucket_name, Key=path) + file['Body'].read() + return True, "" + except ClientError as e: + if e.response['Error']['Code'] == '404': + return False, e.response['Error'] + else: + print(f"An error occurred: {e}") + return False, e.response['Error'] \ No newline at end of file diff --git a/SimvaBrowser/MinioBrowser.py b/SimvaBrowser/MinioBrowser.py new file mode 100644 index 0000000..a72b7e9 --- /dev/null +++ b/SimvaBrowser/MinioBrowser.py @@ -0,0 +1,156 @@ +import requests +import xml.etree.ElementTree as ET +import boto3 +import boto3.session +from botocore.exceptions import ClientError +from jwt import JWT +import os +import json + +class MinioBrowser: + def __init__(self, auth, accept='.json', ca_file=None, delimiter='/', client_secret_file="client_secrets.json"): + basedir = os.path.abspath(f"{os.path.dirname(__file__)}/../") + self.secret_file=self._load_secret_file(os.path.join(basedir, client_secret_file)) + self.auth=auth + self.accept = accept + self.ca_file = ca_file + self.storage_url = self.secret_file.get("minio").get("storage_url") + self.bucket_name = self.secret_file.get("minio").get("bucket_name") + self.access_key_id = self.secret_file.get("minio").get("access_key_id") + self.secret_access_key = self.secret_file.get("minio").get("secret_access_key") + self.traces_folder = self.secret_file.get("minio").get("output_folder") + self.delimiter = delimiter + self.base_path = self.traces_folder + self.delimiter + self.current_path = self.base_path + self.current_level = 0 + + jwt_parser = JWT() + self.jwt = self.auth.get('oidc_auth_token', {}).get("access_token") + self.access_token=jwt_parser.decode(self.jwt, do_verify=False) + self._update_files() + + def _load_secret_file(self, file_path): + with open(file_path, 'r') as file: + secret_data = json.load(file) + return secret_data + + def _storage_login(self): + data = { + 'Action': 'AssumeRoleWithWebIdentity', + 'Version': '2011-06-15', + 'DurationSeconds': 3600, + 'WebIdentityToken': self.auth.get('oidc_auth_token', {}).get("access_token") + } + print(f"Data : {data}") + response = requests.post(self.storage_url, data=data, verify=self.ca_file) + print(f"Response : {response.text}") + + if response.status_code != 200: + print('Problems getting temporary credentials') + print(response.text) + return + + if 'application/xml' not in response.headers['Content-Type']: + print('Expected XML response, got:', response.headers['Content-Type']) + print(response.text) + return + + try: + ns = {'sts': 'https://sts.amazonaws.com/doc/2011-06-15/'} + root = ET.fromstring(response.text) + credentials = root.find('sts:AssumeRoleWithWebIdentityResult', ns).find('sts:Credentials', ns) + self.access_key_id = credentials.find('sts:AccessKeyId', ns).text + self.secret_access_key = credentials.find('sts:SecretAccessKey', ns).text + self.session_token = credentials.find('sts:SessionToken', ns).text + except ET.ParseError as e: + print('Error parsing XML response:', e) + print(response.text) + + def _s3_client(self): + return boto3.client( + 's3', + endpoint_url=self.storage_url, + aws_access_key_id=self.access_key_id, + aws_secret_access_key=self.secret_access_key, + aws_session_token=self.session_token, + verify=self.ca_file + ) + + def _list_files(self, path): + try: + s3_client = self._s3_client() + folder = s3_client.list_objects_v2(Bucket=self.bucket_name, + Prefix=path, + Delimiter=self.delimiter) + files = [] + contents = folder.get('Contents') + print(f"Folder : {folder}") + if contents: + for o in contents: + files.append(o.get('Key')[len(self.current_path):]) + return files + except ClientError as e: + print(f"An error occurred: {e}") + if e.response['Error']['Code'] == 'AccessDenied': + print("Access denied. Check your IAM policies and bucket policies.") + return [] + + def _list_folders(self, path): + try: + s3_client = self._s3_client() + folder = s3_client.list_objects_v2(Bucket=self.bucket_name, + Prefix=path, + Delimiter=self.delimiter) + folders = [] + contents = folder.get('CommonPrefixes') + if contents: + for o in contents: + folders.append(o.get('Prefix')[len(self.current_path):]) + folders=[{"id":dir_id,"name": dir_id} for dir_id in folders] + return folders + except ClientError as e: + print(f"An error occurred: {e}") + if e.response['Error']['Code'] == 'AccessDenied': + print("Access denied. Check your IAM policies and bucket policies.") + return [] + + def get_file_content(self, path): + try: + s3_client = self._s3_client() + file = s3_client.get_object(Bucket=self.bucket_name, Key=path) + return file['Body'].read() + except ClientError as e: + print(f"An error occurred: {e}") + if e.response['Error']['Code'] == 'AccessDenied': + print("Access denied. Check your IAM policies and bucket policies.") + return [] + + def _isdir(self, path): + return path.endswith(self.delimiter) + + def _update_files(self): + self.files = [] + self.dirs = [] + self.current_level=len(self.current_path.replace(self.base_path, "").split("/")) + if self._isdir(self.current_path): + self.dirs = self._list_folders(self.current_path) + self.files = self._list_files(self.current_path) + + def file_exists(self, path): + """ + Check if a file exists in the S3 bucket at the given path. + + :param path: The path of the file in the S3 bucket. + :return: True if the file exists, False otherwise. + """ + try: + s3_client = self._s3_client() + file = s3_client.get_object(Bucket=self.bucket_name, Key=path) + file['Body'].read() + return True, "" + except ClientError as e: + if e.response['Error']['Code'] == '404': + return False, e.response['Error'] + else: + print(f"An error occurred: {e}") + return False, e.response['Error'] \ No newline at end of file diff --git a/SimvaBrowser/SimvaAPIBrowser.py b/SimvaBrowser/SimvaAPIBrowser.py new file mode 100644 index 0000000..46f2449 --- /dev/null +++ b/SimvaBrowser/SimvaAPIBrowser.py @@ -0,0 +1,120 @@ +import requests +from jwt import JWT +import os +import json + +class SimvaBrowser: + def __init__(self, auth, client_secret_file="client_secrets.json"): + basedir = os.path.abspath(f"{os.path.dirname(__file__)}/../") + self.secret_file=self._load_secret_file(os.path.join(basedir, client_secret_file)) + self.auth = auth + self.accepted_activities=[] + self.accepted_studies=[] + self.actual_activity=None + self.actual_study=None + self.actual_selected_file=None + self.simva_api_url = self.secret_file.get("simva").get("api_url") + jwt_parser = JWT() + self.jwt = self.auth.get('oidc_auth_token', {}).get("access_token") + self.access_token=jwt_parser.decode(self.jwt, do_verify=False) + self._load_selected_studies_from_simva_api() + self._load_selected_activities_from_simva_api() + + def _load_secret_file(self, file_path): + with open(file_path, 'r') as file: + secret_data = json.load(file) + return secret_data + + def _load_selected_studies_from_simva_api(self): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}studies" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + username = self.access_token.get('preferred_username') + user_data = [item for item in data if username in item.get("owners",[])] + # Print the result + print("STUDY : Data received:", data) + print("preferred_username:", username) + print("user_data : ", user_data) + self.accepted_studies=user_data + else: + print(f"Error: {response.text}") + + def _load_selected_activities_from_simva_api(self): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}activities" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + username = self.access_token.get('preferred_username') + user_data = [item for item in data if username in item.get("owners",[])] + gameplay_data = [item for item in user_data if item.get("type") == 'gameplay'] + filtered = [item for item in gameplay_data if item.get("extra_data").get("config").get("trace_storage")] + # Print the result + print("ACTIVITIES : Data received:", data) + print("preferred_username:", username) + print("user_data : ", user_data) + print("gameplay_data : ", gameplay_data) + print("filtered : ",filtered) + self.accepted_activities=filtered + else: + print(f"Error: {response.text}") + + def _load_selected_test_from_simva_api(self, testId): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}tests/{testId}" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + # Print the result + print("TEST : Data received:", data) + return data + else: + print(f"Error: {response.text}") + + + def _load_selected_activity_from_simva_api(self, activityId): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}activities:{activityId}" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + # Print the result + print("ACTIVITY : Data received:", data) + return data + else: + print(f"Error: {response.text}") + + +#folders=[{"id":dir.get("_id") + "/","name": dir.get('name')} for dir in self.accepted_activities if dir.get("_id") + "/" in folders] +#if self.current_path == self.base_path: +# self.actual_study=None +# self.actual_activity=None +# self.actual_selected_file=None +#else: +# added_path=self.current_path.replace(self.base_path, "").split("/") +# if len(added_path)>=1 and added_path[0] not in [activity.get("_id") for activity in self.accepted_activities]: +# self.current_path=self.base_path +# self.actual_study=None +# self.actual_activity=None +# self.actual_selected_file=None +# else: +# actual_activity_id=added_path[0] if len(added_path)>=1 else None +# actual_activity_name = [item.get("name") for item in self.accepted_activities if item.get("_id") == actual_activity_id] +# self.actual_activity=f"{actual_activity_name[0]} - {actual_activity_id}" if len(actual_activity_name)>0 else None +# actual_study_id=added_path[1] if len(added_path)>=2 else None +# actual_study_name = [item.get("name") for item in self.accepted_studies if item.get("_id") == actual_study_id] +# self.actual_study=f"{actual_study_name[0]} - {actual_study_id}" if len(actual_study_name)>0 else None +# self.actual_selected_file=added_path[1] if len(added_path)==2 else None +# print("Added Path :") +# print(added_path) + \ No newline at end of file diff --git a/SimvaBrowser/SimvaBrowser.py b/SimvaBrowser/SimvaBrowser.py new file mode 100644 index 0000000..4a78156 --- /dev/null +++ b/SimvaBrowser/SimvaBrowser.py @@ -0,0 +1,253 @@ +import requests +from jwt import JWT +import os +import json + +class SimvaBrowser: + def __init__(self, auth, accept='.json', ca_file=None, delimiter='/', client_secret_file="client_secrets.json"): + #GENERAL + basedir = os.path.abspath(f"{os.path.dirname(__file__)}/../") + self.secret_file=self._load_secret_file(os.path.join(basedir, client_secret_file)) + + #SIMVA + self.auth = auth + self.study_directories=[] + self.accepted_studies=[] + self.accepted_tests=[] + self.accepted_activities=[] + self.actual_study=None + self.actual_test=None + self.actual_activity=None + self.actual_selected_file=None + self.actual_file_url=None + self.simva_api_url = self.secret_file.get("simva").get("api_url") + jwt_parser = JWT() + self.jwt = self.auth.get('oidc_auth_token', {}).get("access_token") + self.access_token=jwt_parser.decode(self.jwt, do_verify=False) + self._load_selected_studies_from_simva_api() + + #MINIO + self.accept = accept + self.ca_file = ca_file + self.traces_folder = "output" + self.delimiter = delimiter + self.base_path = self.traces_folder + self.delimiter + self.current_path = self.base_path + self.current_level = 0 + + self._update_files() + + #GENERAL + def _load_secret_file(self, file_path): + with open(file_path, 'r') as file: + secret_data = json.load(file) + return secret_data + + #SIMVA API Logged + def _load_selected_studies_from_simva_api(self): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}studies" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + # Print the result + print("STUDY : Data received:", data) + self.accepted_studies=data + self.study_directories=[{"id":dir.get("_id") + "/","name": dir.get('name')} for dir in self.accepted_studies] + else: + print(f"Error: {response.text}") + + def _get_accepted_study_from_id(self, studyId): + study=[study for study in self.accepted_studies if study.get("_id") == studyId] + if len(study)>0: + return study[0] + return None + + def _get_accepted_test_from_id(self, testId): + test=[test for test in self.accepted_tests if test.get("_id") == testId] + if len(test)>0: + return test[0] + return None + + def _get_accepted_activity_from_id(self, activityId): + activity=[activity for activity in self.accepted_activities if activity.get("_id") == activityId] + if len(activity)>0: + return activity[0] + return None + + def _load_selected_test_from_simva_api(self, testId): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}studies/{self.actual_study.get('_id')}/tests/{testId}" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + # Print the result + print("TEST : Data received:", data) + return data + else: + print(f"Error: {response.text}") + return None + + def _load_selected_activity_from_simva_api(self, activityId): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}activities/{activityId}" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + # Print the result + print("ACTIVITY : Data received:", data) + return data + else: + print(f"Error: {response.text}") + return None + + def _get_minio_url_from_simva_api(self, activityId): + headers = {'Content-Type': 'application/json'} + if self.jwt: + headers['Authorization'] = f'Bearer {self.jwt}' + url = f"{self.simva_api_url}activities/{activityId}/presignedurl" + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + # Print the result + print("PRESIGNED URL : Data received:", data) + return data + else: + print(f"Error: {response.text}") + return None + + def _list_test_from_study(self, study): + tests=[] + if study is not None: + print(study) + for testid in study.get("tests"): + print("Test :"+ testid) + test=self._load_selected_test_from_simva_api(testid) + if test is not None: + tests.append(test) + return tests + + def _list_activities_from_test(self, test): + activities=[] + if test is not None: + for activityId in test.get("activities"): + print("Activity :"+ activityId) + activity=self._load_selected_activity_from_simva_api(activityId) + print(activity) + if activity is not None: + if activity.get("type") == 'gameplay' and activity.get("extra_data").get("config").get("trace_storage"): + activities.append(activity) + return activities + + def _isdir(self, path): + return path.endswith(self.delimiter) + + def _getStudyIdTestIdAndUpdatedPathFromPath(self,path): + added_path=path.replace(self.base_path, "").split("/") + studyId=None + testId=None + if len(added_path) >= 1: + studyId=added_path[0] + path=path.replace(studyId + "/", "") + if len(added_path) >= 2: + testId=added_path[1] + path=path.replace(testId + "/", "") + return studyId, testId, path + + def _update_files(self): + self.files = [] + self.dirs = [] + self.added_path=self.current_path.replace(self.base_path, "").split("/") + self.current_level=len(self.current_path.replace(self.base_path, "").split("/")) + print("AddedPath") + print(self.added_path) + studyDirs=[{"id":dir.get("_id") + "/","name": dir.get('name')} for dir in self.accepted_studies] + if self.current_level >= 1: + actual_study_id=self.added_path[0] + if actual_study_id not in [study.get("_id") for study in self.accepted_studies]: + self._reset_browser() + if self.current_level >= 2: + self.actual_study=self._get_accepted_study_from_id(actual_study_id) + self.accepted_tests=self._list_test_from_study(self.actual_study) + print(self.accepted_tests) + testDirs=[{"id":dir.get("_id") + "/","name": dir.get('name')} for dir in self.accepted_tests] + if self.current_level >=3: + actual_test_id=self.added_path[1] + self.actual_test=self._get_accepted_test_from_id(actual_test_id) + self.accepted_activities=self._list_activities_from_test(self.actual_test) + activityDirs=[{"id":dir.get("_id") + "/","name": dir.get('name')} for dir in self.accepted_activities] + self.dirs=activityDirs + if self.current_level >=4: + actual_activity_id=self.added_path[2] + self.actual_activity=self._load_selected_activity_from_simva_api(actual_activity_id) + result=self._get_minio_url_from_simva_api(actual_activity_id) + self.actual_file_url=result.get("url") if result is not None else None + else: + self.actual_activity=None + else: + self.actual_test=None + else: + self.actual_study=None + if self.current_level == 0: + print(studyDirs) + self.dirs=studyDirs + self.files=[] + self.actual_file_url=None + elif self.current_level == 2: + print(self.actual_study) + print(testDirs) + self.dirs=testDirs + self.files=[] + self.actual_file_url=None + elif self.current_level == 3: + print(self.actual_test) + print(activityDirs) + self.dirs=activityDirs + self.files=[] + self.actual_file_url=None + else: + if self._isdir(path=self.current_path): + self.dirs=[] + self.files=["traces.json"] if self.actual_file_url is not None else [] + else: + self.dirs=[] + self.files=[] + + def _reset_browser(self): + self.current_path=self.base_path + self.current_level=0 + + self.actual_study=None + self.actual_activity=None + self.actual_test=None + self.actual_selected_file=None + self.actual_file_url=None + + self.accepted_tests=[] + self.accepted_activities=[] + + def get_file_content_from_url(self): + if self.actual_file_url is not None: + try: + # Send an HTTP GET request to the provided URL + response = requests.get(self.actual_file_url) + + # Raise an exception if the request was unsuccessful (HTTP code other than 200) + response.raise_for_status() + print("get_file_content_from_url :") + print(self.actual_file_url) + + # Return the content of the file as text + return self.current_path, response.text + + except requests.exceptions.RequestException as e: + print(f"Error fetching file content: {e}") + return self.current_path, None + else: + return self.current_path, None \ No newline at end of file diff --git a/SimvaBrowser/__init__.py b/SimvaBrowser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/T-Mon.ipynb b/T-Mon.ipynb deleted file mode 100644 index f65c3e2..0000000 --- a/T-Mon.ipynb +++ /dev/null @@ -1,126 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# T-Mon: Traces Monitor in xAPI-SG" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### T-Mon is a set of Jupyter Notebooks to process, analyze and present visualizations of data following the Experience API for Serious Games Profile (xAPI-SG)\n", - "\n", - "The following code:\n", - "- expects as input a **JSON file with a list of xAPI-SG statements**\n", - "- **analyzes the xAPI-SG statements** and fills an adaptation of the **default set of visualizations**\n", - "https://github.com/e-ucm/rage-analytics/wiki/Default-visualizations-teacher\n", - "\n", - "For more information, see **T-Mon GitHub page: https://github.com/e-ucm/t-mon**\n", - "\n", - "\n", - "\n", - "**xAPI-SG main reference:**\n", - "*Applying standards to systematize learning analytics in serious games.\n", - "Ángel Serrano-Laguna, Iván Martínez-Ortiz, Jason Haag, Damon Regan, Andy Johnson, Baltasar Fernández-Manjón\n", - "Computer Standards & Interfaces 50 (2017) 116–123, http://dx.doi.org/10.1016/j.csi.2016.09.014*\n", - "\n", - "Further info on GitHub wiki page: https://github.com/e-ucm/rage-analytics/wiki/xAPI-SG-Profile" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "local = False # Set to True if working with a local-hoster Jupyter server, False if working with a web-hosted Jupyter server\n", - "storage = 'file'\n", - "import json\n", - "import numpy as np\n", - "from datetime import datetime, timedelta\n", - "import copy\n", - "import math\n", - "import matplotlib.dates as mdates\n", - "import matplotlib.pyplot as plt\n", - "from collections import Counter\n", - "import pandas as pd\n", - "from ipywidgets import interact, interactive, fixed, HBox, Layout,VBox\n", - "import ipywidgets as widgets\n", - "from IPython.display import display, clear_output\n", - "import pprint\n", - "plt.style.use('default')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***Display all* function** is constructing an app to display all vis" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "players_info = {} # dict with players info\n", - "global timeformats\n", - "timeformats=['%Y-%m-%dT%H:%M:%SZ','%Y-%m-%dT%H:%M:%S.%fZ'] #array of time format\n", - "\n", - "%run fileBrowserAndUploadButtonToLoadProcessStatements.ipynb\n", - "%run vis/helpersFunctions/clearFigMatplotlib.ipynb\n", - "%run globalsSelectors.ipynb\n", - "\n", - "header=widgets.HTML(\"

T-MON

Select JSON xAPI-SG file to process and see visualisations

\")\n", - "footer=widgets.HTML(\"

T-MON, by eUCM research team

\")\n", - "if storage == 'simva' :\n", - " app=VBox([header,simvaWidget.widget(),footer])\n", - " display(app)\n", - "else:\n", - " if local:\n", - " app=VBox([header,fileBrowser.widget(),footer])\n", - " display(app)\n", - " else:\n", - " app=VBox([header,uploadButtonApp,footer])\n", - " display(app)\n", - " with outTabs:\n", - " display_checkboxes()\n", - " clear_output(wait=True)\n", - " displayvis(None)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/TMonApp.py b/TMonApp.py new file mode 100644 index 0000000..c77f142 --- /dev/null +++ b/TMonApp.py @@ -0,0 +1,16 @@ +from dash import Dash, html +from TMonWidgets import UploadWidget, TMonWidget + +# Initialize the app +TMonApp = Dash(__name__) +# App layout +TMonApp.layout = html.Div([ + TMonWidget.TMonHeader, + UploadWidget.TMonUpload, + TMonWidget.TMonBody, + TMonWidget.TMonFooter + ] +) + +if __name__ == '__main__': + TMonApp.run(debug=True, port="5001") \ No newline at end of file diff --git a/TMonSimvaApp.py b/TMonSimvaApp.py new file mode 100644 index 0000000..28925d3 --- /dev/null +++ b/TMonSimvaApp.py @@ -0,0 +1,21 @@ +from dash import Dash, html +from TMonWidgets import SimvaBrowserWidget, TMonWidget + +# Initialize Dash app with Flask server +app = Dash(__name__, server=SimvaBrowserWidget.flask.flaskServer) + +# Layout of the dashboard +app.layout=html.Div( + [ + SimvaBrowserWidget.LoginLogoutBody, + TMonWidget.TMonHeader, + SimvaBrowserWidget.simvaBrowserBody, + TMonWidget.TMonBody, + TMonWidget.TMonFooter, + ] +) + +if __name__ == '__main__': + # Construct the URL + print(f'The app is running at http://0.0.0.0:8050/') + app.run(debug=True, host="0.0.0.0", port="8050") \ No newline at end of file diff --git a/TMonWidgets/MultiSelector.py b/TMonWidgets/MultiSelector.py new file mode 100644 index 0000000..b6a545a --- /dev/null +++ b/TMonWidgets/MultiSelector.py @@ -0,0 +1,19 @@ +def searchValueFromMultiSelector(df, object, search_value,value): + filtered_df=df + # Make sure that the set values are in the option list, else they will disappear + # from the shown select list, but still part of the `value`. + # Convert the dictionary keys to the appropriate format for dropdown options + all_options = [{'label': k, 'value': k} for k in df[object].unique()] + # Filter options based on the search value + if search_value is not None: + filtered_options = [o for o in all_options if search_value.lower() in o['label'].lower()] + else: + filtered_options = all_options + # Ensure selected values remain in the options list + if value: + selected_options = [o for o in all_options if o['value'] in value] + filtered_options = selected_options + filtered_options + filtered_df = df.loc[df[object].isin(value)] + # Remove duplicates while preserving order + unique_options = list({v['value']:v for v in filtered_options}.values()) + return filtered_df, unique_options \ No newline at end of file diff --git a/TMonWidgets/SimvaBrowserWidget.py b/TMonWidgets/SimvaBrowserWidget.py new file mode 100644 index 0000000..cc3a243 --- /dev/null +++ b/TMonWidgets/SimvaBrowserWidget.py @@ -0,0 +1,322 @@ +from flask import session +import dash +from dash import html, dcc, callback +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate +import json +import TMonWidgets +#Import LoadProcessStatements.py +from LoadProcessStatements import load_from_string, load_players_info_from_uploaded_content +# Import SimvaBrowser class from SimvaBrowser.py +from SimvaBrowser.SimvaBrowser import SimvaBrowser +# Import KeycloakClient class containing a Flask OIDC server from KeycloakClient.py +from SimvaBrowser.KeycloakClient import KeycloakClient +from datetime import datetime +flask=KeycloakClient(homepage=False) + +# Dash callback to handle login button click +@callback( + [Output('login-logout-button', 'children'), + Output('upload-data','style')], + [Input('main-login', 'children')] +) +def login_logout_button_displayed(main): + if flask.oidc.user_loggedin: + return "Logout", {'display': 'None'} + else: + return "Login", {'display': 'block', + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + 'margin': '10px'} + +# Dash callback to handle login button click +@callback( + Output('login-logout', 'children'), + [Input('login-logout-button', 'n_clicks')] +) +def login_logout_button_click(n_clicks): + if n_clicks > 0: + if flask.oidc.user_loggedin: + global browser + browser=None + return dcc.Location(pathname='/logoutkeycloak', id='login-logout-link') + else: + return dcc.Location(pathname='/login', id='login-logout-link') + +# Dash callback to handle account button style +@callback( + [Output('account-button', 'children'), + Output('account-button', 'style')], + [Input('main-login', 'children')] +) +def account_button(main): + if flask.oidc.user_loggedin: + return "Account", {'display': 'block'} + else: + return None, {'display': 'none'} + +# Dash callback to handle login button click +@callback( + Output('account', 'children'), + [Input('account-button', 'n_clicks')] +) +def account_button_click(n_clicks): + if n_clicks > 0: + if flask.oidc.user_loggedin: + return dcc.Location(href=f'{flask.accountpage}', id='account-link') + +# Dash callback to update connection status +@callback( + Output('connection-status', 'children'), + [Input('main-login', 'children')] +) +def update_connection_status(input_value): + if flask.oidc.user_loggedin: + user_info = session.get('oidc_auth_profile', {}) + preferred_username = user_info.get('preferred_username') + return f'Logged in as {preferred_username}' + else: + return 'Not logged in' + +# Dash callback to handle login button click +@callback( + Output('browser_div', 'children'), + Input('main-login', 'children'), + State('url', 'pathname') +) +def init_storage(main, pathname): + if flask.oidc.user_loggedin: + # Initialize SimvaBrowser + global browser + browser = SimvaBrowser(session) + if pathname is None or pathname == '/': + browser.current_path= browser.base_path + else: + # Find the position of "/dashboard/" + index = pathname.find("/dashboard") + # Slice the string up to the index if "/dashboard/" is found + if index != -1: + newpathname = pathname[:index] + else: + newpathname = pathname + if newpathname.endswith(".json/"): + newpathname=newpathname[:((len(newpathname)-1))] + browser.current_path=browser.base_path + newpathname[1:] + print(f"Pathname set to {pathname} - {browser.current_path} - {browser.base_path}") + browser._update_files() + folder_buttons = [html.Button(f"{f.get('name')} ({f.get('id')})", id={'type': 'folder-button', 'index': f.get('id')}, n_clicks=0) for f in browser.dirs] + file_buttons = [html.Button(f, id={'type': 'file-button', 'index': f}, n_clicks=0, style={'backgroundColor': 'green'}) for f in browser.files if f.endswith(browser.accept)] + run_analyse_style = {'display': 'none'} + if not browser._isdir(browser.current_path): + run_analyse_style = {'display': 'block'} + print(f'Study : {browser.actual_study} -Activity : {browser.actual_activity} - File : {browser.actual_selected_file}') + actual_study=browser.actual_study.get("name") if browser.actual_study is not None else "Select a study" + actual_test=browser.actual_test.get("name") if browser.actual_test is not None else "Select an test" + actual_activity=browser.actual_activity.get("name") if browser.actual_activity is not None else "Select an activity" + appLayout = html.Div([ + html.H3(id='current-path', children=browser.current_path, style={'display': 'none'}), + html.H4(id='current-study', children=actual_study), + html.H4(id='current-test', children=actual_test), + html.H4(id='current-activity', children=actual_activity), + html.Button('..', id='parent-directory', n_clicks=0, style={'display': 'block'}), + html.Div(id='folders-div', children=folder_buttons), + html.Div(id='files-div', children=file_buttons), + html.Button('Run Analyse', id='run-analyse', n_clicks=0, style=run_analyse_style), + ]) + return appLayout + else: + return html.Div([ + html.H4("Connect to your SIMVA account to access to your data or Drag and Drop / Select file to visualize your data."), + html.H3(id='current-path', style={'display': 'none'}), + html.H4(id='current-study', style={'display': 'none'}), + html.H4(id='current-test', style={'display': 'none'}), + html.H4(id='current-activity', style={'display': 'none'}), + html.Button('..', id='parent-directory', n_clicks=0, style={'display': 'none'}), + html.Div(id='folders-div', children=[], style={'display': 'none'}), + html.Div(id='files-div', children=[], style={'display': 'none'}), + html.Button('Run Analyse', id='run-analyse', n_clicks=0, style={'display': 'none'}), + ]) + +@callback( + [Output('current-path', 'children'), + Output('current-study', 'children'), + Output('current-test', 'children'), + Output('current-activity', 'children'), + Output('folders-div', 'children'), + Output('files-div', 'children'), + Output('run-analyse', 'style'), + Output('content', 'children'), + Output('output-t-mon', 'style'), + Output('url', 'pathname') + ], + [Input('parent-directory', 'n_clicks'), + Input({'type': 'folder-button', 'index': dash.dependencies.ALL}, 'n_clicks'), + Input({'type': 'file-button', 'index': dash.dependencies.ALL}, 'n_clicks'), + Input('run-analyse', 'n_clicks'), + Input('upload-data', 'contents')], + [State('current-path', 'children'), + State('url', 'pathname'), + State('upload-data', 'filename'), + State('upload-data', 'last_modified')] +) +def update_browser(n_clicks_parent, folder_n_clicks, file_n_clicks, n_clicks_run_analyse, list_of_contents, current_path, statepathname, list_of_names, list_of_dates): + try: + browser + if browser == None: + raise NameError("Browser is null.") + except NameError: + print("Well, it WASN'T defined after all!") + if list_of_names: + print(f"List_of_names : {list_of_names} - list_of_dates : {list_of_dates}") + div_list = [] + TMonWidgets.xapiData = [] + nbError=0 + style={'display': 'block'} + for c, n, d in zip(list_of_contents, list_of_names, list_of_dates): + out, err = [], [] + div_list.append(html.H5(n)) + div_list.append(html.H6(datetime.fromtimestamp(d))) + load_players_info_from_uploaded_content(c, n, TMonWidgets.xapiData, out, err) + div_list.append(html.Div(out)) + div_list.append(html.Div(err)) + if len(err) > 0 : + nbError+=1 + div_list.append(html.Hr()) + if nbError == len(list_of_names): + style={'display': 'none'} + return html.Div(),html.Div(),html.Div(),html.Div(),html.Div(),html.Div(),{'display': 'none'}, div_list, style, f"/dashboard/tab=home_tab" + else: + return html.Div(),html.Div(),html.Div(),html.Div(),html.Div(),html.Div(),{'display': 'none'}, html.Div(), {'display': 'none'}, "/" + else: + print("Sure, it was defined.") + ctx = dash.callback_context + res=f"{current_path} - triggered : {ctx.triggered} - Files : {file_n_clicks} - Folders : {folder_n_clicks} - n_clicks_parent : {n_clicks_parent} - n-clicks-run-analyse : {n_clicks_run_analyse}" + print(res) + if not ctx.triggered: + raise PreventUpdate + triggered_prop_id = ctx.triggered[0]['prop_id'] + print(f'PropId : {triggered_prop_id} - State : {statepathname} - {browser.current_path} - {browser.base_path}') + # Find the position of "/dashboard/" + index = statepathname.find("/dashboard") + # Slice the string up to the index if "/dashboard/" is found + if index != -1: + newstatepathname = statepathname[:index] + dashboard_url = statepathname[index:] + run_dashboard=True + else: + newstatepathname = statepathname + dashboard_url = "" + run_dashboard=False + print(f"run_dashboard : {run_dashboard}") + # Remove the .n_clicks suffix + if 'parent-directory' in triggered_prop_id and int(n_clicks_parent)>0: + print(f"Current Path : {browser.current_path} - State : {newstatepathname} ") + if len(browser.current_path) > len(browser.base_path): + if browser._isdir(browser.current_path): + browser.current_path = browser.current_path.rpartition(browser.delimiter)[0] + browser.current_path = browser.current_path.rpartition(browser.delimiter)[0] + browser.delimiter + else: + browser.current_path = browser.base_path + pathname=browser.current_path.replace(browser.base_path, "/") + print(f"{browser.current_path} - New Path : {pathname}") + elif ('run-analyse' in triggered_prop_id and int(n_clicks_run_analyse)>0) or run_dashboard: + run_analyse_style={'display': 'none'} + folder_buttons=[] + file_buttons=[] + TMonWidgets.xapiData=[] + div_list= [] + out=[] + err=[] + current_file_path, content_string = browser.get_file_content_from_url() + load_from_string( + content_string, TMonWidgets.xapiData, out, err + ) + div_list.append(html.Div([ + html.Div(out), + html.Div(err), + html.Hr(), + ])) + pathname=newstatepathname + if run_dashboard: + dashboardpath=f"{dashboard_url}" + else: + dashboardpath=f"/dashboard/tab=home_tab" + print(f"Pathname : {pathname} - State : {newstatepathname} - dashboardurl : {dashboard_url}") + actual_study=browser.actual_study.get("name") if browser.actual_study is not None else "Select a study" + actual_test=browser.actual_test.get("name") if browser.actual_test is not None else "Select a test" + actual_activity=browser.actual_activity.get("name") if browser.actual_activity is not None else "Select an activity" + if(len(err) > 0): + return browser.current_path,actual_study, actual_test, actual_activity, folder_buttons, file_buttons, run_analyse_style, html.Div(div_list), {'display': 'none'}, f"{pathname}{dashboardpath}" + else: + return browser.current_path,actual_study, actual_test, actual_activity, folder_buttons, file_buttons, run_analyse_style, html.Div(div_list), {'display': 'block'}, f"{pathname}{dashboardpath}" + elif "-button"in triggered_prop_id: + cleaned_prop_id = triggered_prop_id.replace(".n_clicks", "") + button_id = json.loads(cleaned_prop_id) + if button_id['type'] == 'folder-button': + browser.current_path = browser.current_path + button_id['index'] + elif button_id['type'] == 'file-button': + browser.current_path = browser.current_path + button_id["index"] + pathname=browser.current_path.replace(browser.base_path, "/") + else: + print("Nothing to do ! Prevent update") + raise PreventUpdate + browser._update_files() + folder_buttons = [html.Button(f"{f.get('name')} ({f.get('id')})", id={'type': 'folder-button', 'index': f.get("id")}, n_clicks=0) for f in browser.dirs] + file_buttons = [html.Button(f, id={'type': 'file-button', 'index': f}, n_clicks=0, style={'backgroundColor': 'green'}) for f in browser.files if f.endswith(browser.accept)] + run_analyse_style = {'display': 'none'} if browser._isdir(browser.current_path) else {'display': 'block'} + print("Pathname:", pathname) + actual_study=browser.actual_study.get("name") if browser.actual_study is not None else "Select a study" + actual_test=browser.actual_test.get("name") if browser.actual_test is not None else "Select a test" + actual_activity=browser.actual_activity.get("name") if browser.actual_activity is not None else "Select an activity" + return browser.current_path, actual_study,actual_test, actual_activity, folder_buttons, file_buttons, run_analyse_style, html.H1(""), {'display': 'none'}, pathname + +simvaBrowserBody = html.Div( + [ + dcc.Location(id='url', refresh=False), # Location component for URL handling + html.Div(id='browser_div', children=[ + html.H3(id='current-path', style={'display': 'none'}), + html.H4(id='current-study'), + html.H4(id='current-test'), + html.H4(id='current-activity'), + html.Button('..', id='parent-directory', n_clicks=0, style={'display': 'none'}), + html.Div(id='folders-div', children=[]), + html.Div(id='files-div', children=[]), + html.Button('Run Analyse', id='run-analyse', n_clicks=0, style={'display': 'none'}), + ] + ), + dcc.Upload( + id='upload-data', + children=html.Div([ + 'Drag and Drop or ', + html.A('Select Files') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + 'margin': '10px' + }, + # Allow multiple files to be uploaded + multiple=True + ), + html.Div(id='debug-browser', children=[]), + html.Div(id='content',children=[]) + ] +) + +LoginLogoutBody = html.Div(id="main-login", children=[ + html.Button(id='login-logout-button', n_clicks=0), + html.Button(id='account-button', n_clicks=0), + html.Div(id='login-logout'), + html.Div(id='account',children=[]), + html.Div(id='connection-status',children=[]) +]) \ No newline at end of file diff --git a/TMonWidgets/TMonWidget.py b/TMonWidgets/TMonWidget.py new file mode 100644 index 0000000..557d50e --- /dev/null +++ b/TMonWidgets/TMonWidget.py @@ -0,0 +1,272 @@ +import dash +from dash import html, dash_table, dcc, callback, Output, Input, State +from dash.exceptions import PreventUpdate +import pandas as pd +import TMonWidgets +from TMonWidgets.MultiSelector import searchValueFromMultiSelector +from vis import xAPISGPlayersProgress, xAPISGVideosSeenSkipped +from urllib.parse import unquote, urlencode + +def get_value_from_url(url, valueId, urlValuesDelimiter='&'): + decoded_string = unquote(url).replace('+', ' ') + values=decoded_string.split(urlValuesDelimiter) + print(f"{url} - {values}") + for val in values: + index=val.find(valueId) + if index != -1: + return val[index+len(valueId):] + return None + +homepagecontent=[ + html.H2('T-Mon Home Page.'), + html.H3('Please select another tab to see default visualisations with this data.') +] + +@callback( + [ + Output('t-mon-tabs', 'value'), + Output("users-multi-dynamic-dropdown", "value"), + Output("object-multi-dynamic-dropdown", "value") + ], + Input('output-t-mon', 'style'), + State('url-t-mon', 'pathname') +) +def update_tab(style, stateUrl): + if style == {"display":"block"} : + print(f"StateUrl: {stateUrl}") + new_tab=None + urlValues=None + dashboard_data=False + index = stateUrl.find("/dashboard/") + # Slice the string up to the index if "/dashboard/" is found + if index != -1: + urlValues = stateUrl[index + len("/dashboard/"):] + dashboard_data=True + if dashboard_data and urlValues: + new_tab=get_value_from_url(urlValues, "tab=") + users=get_value_from_url(urlValues, "actor.name=") + object=get_value_from_url(urlValues, "object.id=") + else: + new_tab="home_tab" + users=None + object=None + tab=new_tab + user_value=users.split(",") if users else [] + object_value=object.split(",") if object else [] + print(f"Tab: {tab} - URL : {urlValues} - user_value : {user_value} - object_value: {object_value}") + return tab, user_value, object_value + else: + raise PreventUpdate + + +def filterObjectIdDependingTab(df, value, tab): + if tab == "progress_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/serious-game"]["object.id"].unique() + elif tab == "video_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/Cutscene"]["object.id"].unique() + elif tab == "completable_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/level"]["object.id"].unique() + elif tab == "alternative_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/Alternative"]["object.id"].unique() + elif tab == "interaction_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/Screen"]["object.id"].unique() + elif tab == "accessible_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/Screen"]["object.id"].unique() + elif tab == "menu_tab": + vals=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/Screen"]["object.id"].unique() + else: + vals=value + return vals + +@callback( + [ + Output('url-t-mon', 'pathname'), + Output("users-multi-dynamic-dropdown", "options"), + Output('object-multi-dynamic-dropdown', "options"), + Output('tabs-content', 'children'), + ], + Input('t-mon-tabs', 'value'), + Input("users-multi-dynamic-dropdown", "search_value"), + Input("users-multi-dynamic-dropdown", "value"), + Input("object-multi-dynamic-dropdown", "search_value"), + Input("object-multi-dynamic-dropdown", "value"), +) +def update_output(tab, user_search_value, user_value, object_search_value, object_value): + ctx = dash.callback_context + triggered=ctx.triggered + triggered_prop_id = ctx.triggered[0]['prop_id'] + res=f"Triggered : {triggered} - {triggered_prop_id}" + print(res) + # Normalize the JSON data to a pandas DataFrame + if 'object-multi-dynamic-dropdown' in triggered_prop_id or 'users-multi-dynamic-dropdown' in triggered_prop_id or 't-mon-tabs' in triggered_prop_id: + if len(TMonWidgets.xapiData) > 0: + df = pd.json_normalize(TMonWidgets.xapiData) + filtered_df, user_unique_options=searchValueFromMultiSelector(df, "actor.name", user_search_value, user_value) + filtered_df, object_unique_options=searchValueFromMultiSelector(filtered_df, "object.id", object_search_value, object_value) + object_unique_options=filterObjectIdDependingTab(df, object_unique_options, tab) + if tab == 'home_tab': + tab_content = html.Div(html.Div(homepagecontent)) + elif tab == 'progress_tab': + content=[] + content.append(html.H3('Player Progress Throw Serious game')) + seriousgamesId=df.loc[df["object.definition.type"]=="https://w3id.org/xapi/seriousgames/activity-types/serious-game"]['object.id'].unique() + for game in seriousgamesId: + bargamedata=xAPISGPlayersProgress.ProgressPlayerLineChart(filtered_df, game) + fig=xAPISGPlayersProgress.displayPlayerProgressFig( + bar_game_data=bargamedata, + game=game + ) + content.append(dcc.Graph(id=f"barchart-{game}",figure=fig)) + fig=xAPISGPlayersProgress.displayPlayerProgressInitFig( + bar_game_data=bargamedata, + game=game + ) + content.append(dcc.Graph(id=f"barchart-init-{game}",figure=fig)) + fig=xAPISGPlayersProgress.displayPlayerProgressPieFig( + pie_chart_data=xAPISGPlayersProgress.ProgressPlayerPie(filtered_df, game), + game=game + ) + content.append(dcc.Graph(id=f"pie-{game}",figure=fig)) + tab_content = html.Div(html.Div(content)) + elif tab == 'video_tab': + tab_content= html.Div([ + html.H3('Video Seen/Skipped'), + dcc.Graph( + id='video-skipped-seen', + figure=xAPISGVideosSeenSkipped.VideoSeenSkippedBarChart(filtered_df) + ) + ]) + elif tab == 'completable_tab': + tab_content= html.Div([ + html.H3('Tab content 3'), + dcc.Graph( + id='graph-2-tabs-dcc', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + elif tab == 'alternative_tab': + tab_content= html.Div([ + html.H3('Tab content 4'), + dcc.Graph( + id='graph-2-tabs-dcc', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + elif tab == 'interaction_tab': + tab_content= html.Div([ + html.H3('Tab content 5'), + dcc.Graph( + id='graph-2-tabs-dcc', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + elif tab == 'accessible_tab': + tab_content= html.Div([ + html.H3('Tab content 6'), + dcc.Graph( + id='graph-2-tabs-dcc', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + elif tab == 'menu_tab': + tab_content= html.Div([ + html.H3('Tab content 7'), + dcc.Graph( + id='graph-2-tabs-dcc', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + elif tab == 'data_tab': + # Convert the DataFrame to a dictionary suitable for DataTable + data = filtered_df.to_dict('records') + tab_content= html.Div([ + html.H3("Length table : " + str(len(data))), + dash_table.DataTable( + id='table-all-xapi-data', + columns=[{"name": i, "id": i} for i in filtered_df.columns], + data=data, + filter_action='native', + sort_action="native", + sort_mode="multi", + #sort_by=[{'column_id': 'timestamp', 'direction': 'asc'}], + ) + ]) + else: + tab_content = html.Div() + new_query_params = { 'tab': tab } + if user_value and len(user_value)>0: + new_query_params["actor.name"]=",".join(user_value) + if object_value and len(object_value)>0: + new_query_params["object.id"]=",".join(object_value) + print(f"Query param url:{new_query_params}") + # Construct a new query string with unique parameters + new_query_string = urlencode(new_query_params, safe=" ") + print(f"new_query_string: {new_query_string}") + return f"{new_query_string}", user_unique_options,object_unique_options, tab_content + else: + url=f"tab=home_tab" + return url,[], [], html.Div(homepagecontent) + else: + raise PreventUpdate + +TMonHeader=html.Div([ + html.H1(children='T-Mon'), + html.Hr(), + html.H2(children='Select JSON xAPI-SG file to process and see visualizations'), +]) + +TMonBody=html.Div([ + dcc.Location(id='url-t-mon', refresh=False), # Location component for URL handling + html.Div(id='output-t-mon', style={'display': 'none'}, children=[ + dcc.Dropdown(id='users-multi-dynamic-dropdown', multi=True), + dcc.Dropdown(id='object-multi-dynamic-dropdown', multi=True), + html.Div( + dcc.Tabs(id="t-mon-tabs", value="home_tab", children=[ + dcc.Tab(label='HomePage', value='home_tab'), + dcc.Tab(label='Progress', value='progress_tab'), + dcc.Tab(label='Videos', value='video_tab'), + dcc.Tab(label='Completable', value='completable_tab'), + dcc.Tab(label='Alternatives', value='alternative_tab'), + dcc.Tab(label='Interactions', value='interaction_tab'), + dcc.Tab(label='Accessible', value='accessible_tab'), + dcc.Tab(label='Menu', value='menu_tab'), + dcc.Tab(label='xAPI Data', value='data_tab') + ]) + ), + html.Div(id="tabs-content") + ]) +]) +TMonFooter=html.Div([ + html.Hr(), + html.H4(children='T-MON, by eUCM research team') +]) diff --git a/TMonWidgets/UploadWidget.py b/TMonWidgets/UploadWidget.py new file mode 100644 index 0000000..501811e --- /dev/null +++ b/TMonWidgets/UploadWidget.py @@ -0,0 +1,61 @@ +from dash import html, dcc, callback, Output, Input, State +from LoadProcessStatements import load_players_info_from_uploaded_content +import datetime +from dash.exceptions import PreventUpdate +import TMonWidgets + +TMonUpload=html.Div([ + dcc.Location(id="url", refresh=False), + dcc.Upload( + id='upload-data', + children=html.Div([ + 'Drag and Drop or ', + html.A('Select Files') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + 'margin': '10px' + }, + # Allow multiple files to be uploaded + multiple=True + ), + html.Div(id='output-treatment'), +]) + +@callback( + [ + Output('output-treatment', 'children'), + Output('output-t-mon', 'style') + ], + Input('upload-data', 'contents'), + State('upload-data', 'filename'), + State('upload-data', 'last_modified') +) +def update_output(list_of_contents, list_of_names, list_of_dates): + if list_of_contents is None: + return [], {'display': 'none'} + else: + div_list = [] + TMonWidgets.xapiData = [] + nbError=0 + style={'display': 'block'} + for c, n, d in zip(list_of_contents, list_of_names, list_of_dates): + out, err = [], [] + div_list.append(html.H5(n)) + div_list.append(html.H6(datetime.datetime.fromtimestamp(d))) + load_players_info_from_uploaded_content(c, n, TMonWidgets.xapiData, out, err) + div_list.append(html.Div(out)) + div_list.append(html.Div(err)) + if len(err) > 0 : + nbError+=1 + div_list.append(html.Hr()) + if nbError == len(list_of_names): + style={'display': 'none'} + return div_list, style + diff --git a/TMonWidgets/__init__.py b/TMonWidgets/__init__.py new file mode 100644 index 0000000..b5e61fa --- /dev/null +++ b/TMonWidgets/__init__.py @@ -0,0 +1 @@ +xapiData=[] \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client_secrets_example.json b/client_secrets_example.json new file mode 100644 index 0000000..3538033 --- /dev/null +++ b/client_secrets_example.json @@ -0,0 +1,21 @@ +{ + "web": { + "secure": "false", + "host": "0.0.0.0", + "port": "8050", + "debug": "true", + "issuer": "https://<>.<>/realms/<>", + "auth_uri": "https://<>.<>/realms/<>/protocol/openid-connect/auth", + "client_id": "<>", + "client_secret": "<>", + "redirect_uris": [ + "https://<>.<>/*" + ], + "userinfo_uri": "https://<>.<>/realms/<>/protocol/openid-connect/userinfo", + "token_uri": "https://<>.<>/realms/<>/protocol/openid-connect/token", + "token_introspection_uri": "https://<>.<>/realms/<>/protocol/openid-connect/token/introspect" + }, + "simva": { + "api_url": "https://<>.<>/" + } +} \ No newline at end of file diff --git a/demo.ipynb b/demo.ipynb index 4f21788..ae4f25f 100644 --- a/demo.ipynb +++ b/demo.ipynb @@ -2,65 +2,29 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Info log (2 lines):\n", - " ... 1st line is valid JSON; interpreting as one-statement-per-line (84 statement(s))\n", - "... processed 84/84 statement(s) in 0:00:00.002255. Displaying visualizations ...\n" - ] - } - ], + "outputs": [], "source": [ "# cargamos un fichero con trazas, usando código de t-mon\n", - "\n", - "%run ProcessxAPISGStatement.py\n", - "%run fileBrowserAndUploadButtonToLoadProcessStatements.py\n", - "global players_info\n", - "players_info= load_players_info_from_file(\"xapi-sg-sample-data.json\")\n", - "players_info" + "from LoadProcessStatements import load_players_info_from_file\n", + "out, err, xapiData =[],[], []\n", + "df=load_players_info_from_file(\"xapi-sg-sample-data-array.json\", xapiData, out, err)\n", + "print(out, err)\n", + "#xapiData" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# cargamos pandas, plotly\n", - "\n", "import pandas as pd\n", "import numpy as np\n", "import plotly.graph_objs as go\n", + "import plotly.express as px\n", "from plotly.subplots import make_subplots\n", "from plotly.offline import init_notebook_mode, iplot\n", "init_notebook_mode(connected=True)" @@ -68,51 +32,86 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "object of type 'NoneType' has no len()", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m~\\AppData\\Local\\Temp\\ipykernel_27168\\2959794928.py\u001b[0m in \u001b[0;36m?\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m# montemos un dataframe\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mdf\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpd\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mplayers_info\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'index'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[0mdf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mloc\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'John Smith'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32mc:\\github\\.venv\\Lib\\site-packages\\pandas\\core\\frame.py\u001b[0m in \u001b[0;36m?\u001b[1;34m(cls, data, orient, dtype, columns)\u001b[0m\n\u001b[0;32m 1894\u001b[0m \"\"\"\n\u001b[0;32m 1895\u001b[0m \u001b[0mindex\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1896\u001b[0m \u001b[0morient\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0morient\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# type: ignore[assignment]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1897\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0morient\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m\"index\"\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m-> 1898\u001b[1;33m \u001b[1;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1899\u001b[0m \u001b[1;31m# TODO speed up Series case\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1900\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0miter\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mSeries\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1901\u001b[0m \u001b[0mdata\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0m_from_nested_dict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mTypeError\u001b[0m: object of type 'NoneType' has no len()" - ] - } - ], + "outputs": [], "source": [ - "# montemos un dataframe\n", - "df = pd.DataFrame.from_dict(players_info, 'index')\n", - "df.loc['John Smith']" + "df = pd.json_normalize(xapiData)\n", + "df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "total de 5 actores\n" - ] - } - ], + "outputs": [], "source": [ "# algo de información básica\n", - "print(f\"total de {len(df)} actores\")" + "print(f\"total of {len(df)} lines and {len(df['actor.name'].unique())} actors\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "progress_verbs=[\"http://adlnet.gov/expapi/verbs/initialized\", \"http://adlnet.gov/expapi/verbs/progressed\", \"http://adlnet.gov/expapi/verbs/completed\"]\n", + "progress_data=df.loc[df['verb.id'].isin(progress_verbs)].copy()\n", + "progress_data\n", + "result_progress_columns = [col for col in df.columns if 'result' in col and 'progress' in col]\n", + "print(result_progress_columns[0])\n", + "# Define a function to update the progress for specific verbs\n", + "def update_progress(row):\n", + " if row['verb.id'] == \"http://adlnet.gov/expapi/verbs/initialized\":\n", + " return 0\n", + " elif row['verb.id'] == \"http://adlnet.gov/expapi/verbs/completed\":\n", + " return 1\n", + " else:\n", + " return row[result_progress_columns[0]]\n", + "progress_data[result_progress_columns[0]] = progress_data.apply(update_progress, axis=1)\n", + "\n", + "actors=progress_data['actor.name'].unique()\n", + "\n", + "object_id_levels=progress_data['object.id'].unique()\n", + "\n", + "for game in object_id_levels:\n", + " bar_game_data=progress_data.loc[progress_data['object.id'] == game].copy()\n", + " data = []\n", + " data_init = []\n", + " for actor in bar_game_data['actor.name'].unique():\n", + " bar_data=bar_game_data.loc[bar_game_data['actor.name'] == actor].copy()\n", + " data.append(go.Scatter(x=bar_data['timestamp'], y=bar_data[result_progress_columns[0]], name=actor, hovertext=f\"{actor}\", mode=\"lines+markers\"))\n", + " first =bar_data.timestamp.min()\n", + " bar_data.timestamp = pd.to_timedelta(pd.to_datetime(bar_data['timestamp']) - pd.to_datetime(first)) + pd.to_datetime('1970/01/01')\n", + " data_init.append(go.Scatter(\n", + " x=bar_data['timestamp'], \n", + " y=bar_data[result_progress_columns[0]], \n", + " name=actor, \n", + " hovertext=f\"{actor}\", \n", + " mode=\"lines+markers\",\n", + " hovertemplate=\"%{x|%H:%M:%S.%L} value: %{y}\"\n", + " ))\n", + " #\n", + " fig = go.Figure(\n", + " layout_title_text='Progress during game level ' + str(game),\n", + " data=data\n", + " )\n", + " fig.update_xaxes(categoryorder=\"total descending\")\n", + " fig.show()\n", + " fig2 = go.Figure(\n", + " layout_title_text='Progress (same origin) during game level ' + str(game),\n", + " data=data_init\n", + " )\n", + " fig2.update_xaxes(categoryorder=\"total descending\")\n", + " fig2.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Intentemos ver puntuación en cada completable..." + "## Progress Data" ] }, { @@ -121,2130 +120,159 @@ "metadata": {}, "outputs": [], "source": [ - "# montamos un dataframe para valores de este gráfico en concreto\n", - "cvalues = []\n", - "for id, row in df[['completables_scores']].iterrows():\n", - " for k, v in row['completables_scores'].items():\n", - " cvalues.append({'id': id, 'completable': k, 'score': v})\n", - "cvalues = pd.DataFrame.from_records(cvalues)" + "progress_game=progress_data.loc[~progress_data['object.id'].str.contains('Levels', case=False)]\n", + "# Filter for \"started\" and \"completed\"\n", + "progress_game_started = progress_game[progress_game['verb.id'].str.contains('initialized')]\n", + "progress_game_completed = progress_game[progress_game['verb.id'].str.contains('completed')]\n", + "\n", + "# Merge to find actors who started and completed the same object\n", + "progress_game_merged = pd.merge(progress_game_started, progress_game_completed, on=['actor.name'], suffixes=('_started', '_completed'))\n", + "\n", + "# Get the count of unique actors who started and completed the same object\n", + "actor_started_completed = progress_game_merged['actor.name'].unique()\n", + "\n", + "# Find actors who have not completed the same object they started\n", + "progress_game_only_started = progress_game_started[~progress_game_started['actor.name'].isin(progress_game_completed['actor.name'])]\n", + "\n", + "# Get the count of unique actors who only started an object\n", + "actor_only_started = progress_game_only_started['actor.name'].unique()\n", + "\n", + "print(f\"Number of actors who started and completed the same object: {len(actor_started_completed)}\")\n", + "print(f\"Number of actors who only started: {len(actor_only_started)}\")\n", + "\n", + "# Create data for pie chart\n", + "data = {\n", + " 'Category': ['Started and Completed', 'Only Started'],\n", + " 'Count': [len(actor_started_completed), len(actor_only_started)],\n", + " 'Actors': [', '.join(actor_started_completed), ', '.join(actor_only_started)]\n", + "}\n", + "\n", + "df_pie = pd.DataFrame(data)\n", + "\n", + "# Create pie chart with Plotly\n", + "fig = px.pie(df_pie, values='Count', names='Category', title='Actors by Activity Status', \n", + " hover_data=['Actors'], labels={'Actors': 'Actors'})\n", + "\n", + "# Show the plot\n", + "fig.show()\n", + "\n", + "fig = go.Figure(data=[go.Pie(\n", + " labels=df_pie['Category'],\n", + " values=df_pie['Count'],\n", + " hoverinfo='label+percent+value+text',\n", + " text=df_pie['Actors']\n", + ")])\n", + "\n", + "fig.update_layout(\n", + " title_text='Actors by Activity Status'\n", + ")\n", + "\n", + "# Show the plot\n", + "fig.show()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "name": "World1-1", - "type": "bar", - "x": [ - "John Smith", - "Sarah Connor", - "Juan Gomez" - ], - "y": [ - 50, - 17, - 22 - ] - }, - { - "name": "MyFirstGame", - "type": "bar", - "x": [ - "John Smith", - "Sarah Connor", - "James Dean", - "Mary Kate", - "Juan Gomez" - ], - "y": [ - 50, - 33, - 95, - 72, - 43 - ] - }, - { - "name": "World1-2", - "type": "bar", - "x": [ - "James Dean", - "Mary Kate", - "Juan Gomez" - ], - "y": [ - 95, - 72, - 60 - ] - } - ], - "layout": { - "barmode": "stack", - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Puntuación en completables" - }, - "xaxis": { - "categoryorder": "total descending" - } - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "# mostramos\n", - "data = []\n", - "for c in cvalues['completable'].unique():\n", - " bar_data = cvalues[cvalues['completable'] == c]\n", - " data.append(go.Bar(x=bar_data['id'], y=bar_data['score'], name=c)) \n", - "fig = go.Figure(\n", - " layout_title_text=\"Puntuación en completables\",\n", - " data=data\n", + "# Define a function to update the progress for specific verbs\n", + "def update_started_completed_progress(row):\n", + " if row['timestamp_started'] and row['timestamp_completed']:\n", + " return f\"Started And Completed\"\n", + " else:\n", + " return f\"Only Started\"\n", + "progress_game=progress_data.loc[progress_data[\"object.definition.type\"].str.contains(\"serious-game\")]\n", + "# Filter for \"started\" and \"completed\"\n", + "progress_game_started = progress_game[progress_game['verb.id'].str.contains('initialized')]\n", + "progress_game_completed = progress_game[progress_game['verb.id'].str.contains('completed')]\n", + "\n", + "# Merge to find actors who started and completed the same object\n", + "progress_game_merged = pd.merge(progress_game_started, progress_game_completed, on=['actor.name'], suffixes=('_started', '_completed'))\n", + "\n", + "progress_game_merged[\"startedCompleted\"] = progress_game_merged.apply(update_started_completed_progress, axis=1)\n", + "pie_chart=progress_game_merged.groupby([\"startedCompleted\"])[\"actor.name\"].apply(list).reset_index(name='Actors')\n", + "pie_chart[\"count\"]=len(pie_chart[\"Actors\"][0])\n", + "pie_chart\n", + "fig = go.Figure(data=[go.Pie(\n", + " labels=pie_chart['startedCompleted'],\n", + " values=pie_chart['count'],\n", + " hoverinfo='label+percent+value+text',\n", + " text=pie_chart['Actors']\n", + ")])\n", + "\n", + "fig.update_layout(\n", + " title_text='Game Status'\n", ")\n", - "fig.update_xaxes(categoryorder=\"total descending\")\n", - "fig.update_layout(barmode='stack')\n", + "\n", + "## Show the plot\n", "fig.show()" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "### Y ahora, veamos progreso en el tiempo, con y sin alinear al inicio del experimento" + "only_cutscenes = df[df['object.definition.type'].str.contains('cutscene', case=False)]\n", + "\n", + "df_skipped = only_cutscenes[only_cutscenes['verb.id'].str.contains('skipped')]\n", + "df_accessed = only_cutscenes[only_cutscenes['verb.id'].str.contains('accessed')]\n", + "print(\"Skipped \" + str(len(df_skipped)))\n", + "print(\"Access \" + str(len(df_accessed)))\n", + "# Merge to find actors who start and skipped the same object\n", + "df_access_skipped = pd.merge(df_skipped, df_accessed, on=['object.id','actor.name'], how= 'outer', suffixes=('_skipped', '_accessed'))\n", + "df_access_skipped\n", + "\n", + "# Group by 'object' and 'actor' to get the count\n", + "df_grouped = df_access_skipped[['object.id', 'actor.name', 'verb.id_skipped', 'verb.id_accessed']].groupby(['object.id', 'actor.name']).count().reset_index()\n", + "df_grouped['count_seen_but_skipped']=df_grouped['verb.id_skipped']\n", + "df_grouped['count_seen']=df_grouped['verb.id_accessed']-df_grouped['verb.id_skipped']\n", + "\n", + "# Create a stacked bar chart with Plotly\n", + "fig = px.bar(df_grouped, x='object.id', y=['count_seen', 'count_seen_but_skipped'], \n", + " color_discrete_sequence=['#1f77b4', '#ff7f0e'],\n", + " hover_name='actor.name', \n", + " labels={'count_seen_but_skipped': 'Access Count', 'count_seen': 'Seen Count', 'actor.name':'Actor'})\n", + "### Show the plot\n", + "fig.show()" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idcompletableprogresstime
0John SmithMyFirstGame0.152016-05-24 15:05:49
1John SmithMyFirstGame0.502016-05-24 15:15:49
2John SmithMyFirstGame0.702016-05-24 15:25:49
3John SmithMyFirstGame0.902016-05-24 15:35:49
4Sarah ConnorMyFirstGame0.102016-05-24 15:05:49
5Sarah ConnorMyFirstGame0.352016-05-24 15:15:49
6Sarah ConnorMyFirstGame0.552016-05-24 15:25:49
7Sarah ConnorMyFirstGame0.752016-05-24 15:35:49
8James DeanMyFirstGame0.452016-05-24 15:05:49
9James DeanMyFirstGame0.802016-05-24 15:15:49
10James DeanMyFirstGame0.992016-05-24 15:35:49
11Mary KateMyFirstGame0.252016-05-24 15:36:49
12Mary KateMyFirstGame0.352016-05-24 15:38:49
13Mary KateMyFirstGame0.652016-05-24 15:45:49
14Mary KateMyFirstGame0.752016-05-24 15:47:49
15Juan GomezMyFirstGame0.302016-05-24 15:17:49
16Juan GomezMyFirstGame0.502016-05-24 15:25:49
17Juan GomezMyFirstGame0.752016-05-24 15:33:49
18Juan GomezMyFirstGame0.952016-05-24 15:40:49
\n", - "
" - ], - "text/plain": [ - " id completable progress time\n", - "0 John Smith MyFirstGame 0.15 2016-05-24 15:05:49\n", - "1 John Smith MyFirstGame 0.50 2016-05-24 15:15:49\n", - "2 John Smith MyFirstGame 0.70 2016-05-24 15:25:49\n", - "3 John Smith MyFirstGame 0.90 2016-05-24 15:35:49\n", - "4 Sarah Connor MyFirstGame 0.10 2016-05-24 15:05:49\n", - "5 Sarah Connor MyFirstGame 0.35 2016-05-24 15:15:49\n", - "6 Sarah Connor MyFirstGame 0.55 2016-05-24 15:25:49\n", - "7 Sarah Connor MyFirstGame 0.75 2016-05-24 15:35:49\n", - "8 James Dean MyFirstGame 0.45 2016-05-24 15:05:49\n", - "9 James Dean MyFirstGame 0.80 2016-05-24 15:15:49\n", - "10 James Dean MyFirstGame 0.99 2016-05-24 15:35:49\n", - "11 Mary Kate MyFirstGame 0.25 2016-05-24 15:36:49\n", - "12 Mary Kate MyFirstGame 0.35 2016-05-24 15:38:49\n", - "13 Mary Kate MyFirstGame 0.65 2016-05-24 15:45:49\n", - "14 Mary Kate MyFirstGame 0.75 2016-05-24 15:47:49\n", - "15 Juan Gomez MyFirstGame 0.30 2016-05-24 15:17:49\n", - "16 Juan Gomez MyFirstGame 0.50 2016-05-24 15:25:49\n", - "17 Juan Gomez MyFirstGame 0.75 2016-05-24 15:33:49\n", - "18 Juan Gomez MyFirstGame 0.95 2016-05-24 15:40:49" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "# montamos un dataframe para valores de este gráfico en concreto\n", - "tvalues = []\n", - "for id, row in df[['completables_progress']].iterrows():\n", - " for v in row['completables_progress']['MyFirstGame']:\n", - " tvalues.append({'id': id, 'completable': 'MyFirstGame', 'progress': v[0], 'time': np.datetime64(v[1])})\n", - "tvalues = pd.DataFrame.from_records(tvalues)\n", - "tvalues" + "### Intentemos ver puntuación en cada completable..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hovertext": "John Smith", - "mode": "lines+markers", - "name": "John Smith", - "type": "scatter", - "x": [ - "2016-05-24T15:05:49", - "2016-05-24T15:15:49", - "2016-05-24T15:25:49", - "2016-05-24T15:35:49" - ], - "y": [ - 0.15, - 0.5, - 0.7, - 0.9 - ] - }, - { - "hovertext": "Sarah Connor", - "mode": "lines+markers", - "name": "Sarah Connor", - "type": "scatter", - "x": [ - "2016-05-24T15:05:49", - "2016-05-24T15:15:49", - "2016-05-24T15:25:49", - "2016-05-24T15:35:49" - ], - "y": [ - 0.1, - 0.35, - 0.55, - 0.75 - ] - }, - { - "hovertext": "James Dean", - "mode": "lines+markers", - "name": "James Dean", - "type": "scatter", - "x": [ - "2016-05-24T15:05:49", - "2016-05-24T15:15:49", - "2016-05-24T15:35:49" - ], - "y": [ - 0.45, - 0.8, - 0.99 - ] - }, - { - "hovertext": "Mary Kate", - "mode": "lines+markers", - "name": "Mary Kate", - "type": "scatter", - "x": [ - "2016-05-24T15:36:49", - "2016-05-24T15:38:49", - "2016-05-24T15:45:49", - "2016-05-24T15:47:49" - ], - "y": [ - 0.25, - 0.35, - 0.65, - 0.75 - ] - }, - { - "hovertext": "Juan Gomez", - "mode": "lines+markers", - "name": "Juan Gomez", - "type": "scatter", - "x": [ - "2016-05-24T15:17:49", - "2016-05-24T15:25:49", - "2016-05-24T15:33:49", - "2016-05-24T15:40:49" - ], - "y": [ - 0.3, - 0.5, - 0.75, - 0.95 - ] - } - ], - "layout": { - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Progreso en 1er completable" - }, - "xaxis": { - "categoryorder": "total descending" - } - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "# mostramos\n", + "df_completable= df[df['verb.id'].str.contains('completed')]\n", + "df_completable\n", + "\n", + "score_columns = [col for col in df_completable.columns if 'score' in col]\n", + "if len(score_columns) > 0:\n", + " print(score_columns[0])\n", + " df_completable[score_columns[0]]\n", + "\n", "data = []\n", - "for id in tvalues['id'].unique():\n", - " bar_data = tvalues[tvalues['id'] == id]\n", - " data.append(go.Scatter(x=bar_data['time'], y=bar_data['progress'], name=id, hovertext=f\"{id}\", mode=\"lines+markers\")) \n", + "for c in df_completable['actor.name'].unique():\n", + " bar_data = df_completable[df_completable['actor.name'] == c]\n", + " data.append(go.Bar(x=bar_data['object.id'], y=bar_data[score_columns[0]], name=c))\n", + "\n", "fig = go.Figure(\n", - " layout_title_text=\"Progreso en 1er completable\",\n", + " layout_title_text=\"Score by completable\",\n", " data=data\n", ")\n", "fig.update_xaxes(categoryorder=\"total descending\")\n", + "fig.update_layout(barmode='group')\n", + "### Show the plot\n", "fig.show()" ] }, @@ -2252,1095 +280,22 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hovertext": "John Smith", - "mode": "lines+markers", - "name": "John Smith", - "type": "scatter", - "x": [ - "1970-01-01T00:00:00", - "1970-01-01T00:10:00", - "1970-01-01T00:20:00", - "1970-01-01T00:30:00" - ], - "y": [ - 0.15, - 0.5, - 0.7, - 0.9 - ] - }, - { - "hovertext": "Sarah Connor", - "mode": "lines+markers", - "name": "Sarah Connor", - "type": "scatter", - "x": [ - "1970-01-01T00:00:00", - "1970-01-01T00:10:00", - "1970-01-01T00:20:00", - "1970-01-01T00:30:00" - ], - "y": [ - 0.1, - 0.35, - 0.55, - 0.75 - ] - }, - { - "hovertext": "James Dean", - "mode": "lines+markers", - "name": "James Dean", - "type": "scatter", - "x": [ - "1970-01-01T00:00:00", - "1970-01-01T00:10:00", - "1970-01-01T00:30:00" - ], - "y": [ - 0.45, - 0.8, - 0.99 - ] - }, - { - "hovertext": "Mary Kate", - "mode": "lines+markers", - "name": "Mary Kate", - "type": "scatter", - "x": [ - "1970-01-01T00:00:00", - "1970-01-01T00:02:00", - "1970-01-01T00:09:00", - "1970-01-01T00:11:00" - ], - "y": [ - 0.25, - 0.35, - 0.65, - 0.75 - ] - }, - { - "hovertext": "Juan Gomez", - "mode": "lines+markers", - "name": "Juan Gomez", - "type": "scatter", - "x": [ - "1970-01-01T00:00:00", - "1970-01-01T00:08:00", - "1970-01-01T00:16:00", - "1970-01-01T00:23:00" - ], - "y": [ - 0.3, - 0.5, - 0.75, - 0.95 - ] - } - ], - "layout": { - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Progreso en 1er completable, desde inicio de sesión" - }, - "xaxis": { - "categoryorder": "total descending" - } - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "# y ahora, alineando por 1er evento\n", "data = []\n", - "for id in tvalues['id'].unique():\n", - " bar_data = tvalues[tvalues['id'] == id].copy()\n", - " first = bar_data.time.min()\n", - " bar_data.time = pd.to_timedelta(bar_data.time - first) + pd.to_datetime('1970/01/01')\n", - " data.append(go.Scatter(x=bar_data['time'], y=bar_data['progress'], name=id, hovertext=f\"{id}\", mode=\"lines+markers\")) \n", + "for c in df_completable['object.id'].unique():\n", + " bar_data = df_completable[df_completable['object.id'] == c]\n", + " data.append(go.Bar(x=bar_data['actor.name'], y=bar_data[score_columns[0]], name=c))\n", + " \n", "fig = go.Figure(\n", - " layout_title_text=\"Progreso en 1er completable, desde inicio de sesión\",\n", + " layout_title_text=\"Score by completable\",\n", " data=data\n", ")\n", "fig.update_xaxes(categoryorder=\"total descending\")\n", + "fig.update_layout(barmode='group')\n", + "### Show the plot\n", "fig.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------------------------------------------------------------------------\n", - "TypeError Traceback (most recent call last)\n", - "Cell In[32], line 55, in update_output(\n", - " list_of_contents=['data:application/json;base64,eyJhY3RvciI6eyJuYW1...sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0K'],\n", - " list_of_names=['xapi-sg-sample-data.json'],\n", - " list_of_dates=[1715587109.421]\n", - ")\n", - " 48 @callback(Output('output-data-upload', 'children'),\n", - " 49 Input('upload-data', 'contents'),\n", - " 50 State('upload-data', 'filename'),\n", - " 51 State('upload-data', 'last_modified'))\n", - " 52 def update_output(list_of_contents, list_of_names, list_of_dates):\n", - " 53 if list_of_contents is not None:\n", - " 54 return [\n", - "---> 55 fileBrowserAndUploadButtonToLoadProcessStatements.load_players_info_from_content(c, n, d) for c, n, d in\n", - " c = np.str_('World1-2')\n", - " list_of_contents = ['data:application/json;base64,eyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuMTV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC43fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjl9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTozNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL2V4dC92YWx1ZSI6IkdhbWUgT3ZlciIsInN1Y2Nlc3MiOiJUcnVlIiwic2NvcmUiOjUwfX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NDA6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IlRydWUiLCJzY29yZSI6NTB9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9TY3JlZW5zL1NvdW5kTWVudSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL1NjcmVlbiJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKb2huIFNtaXRoIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9jdXRzY2VuZXMvSW50cm9WaWRlbyIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0N1dHNjZW5lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9WaWRlb1R1dG9yaWFsIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2lkLnRpbmNhbmFwaS5jb20vdmVyYi9za2lwcGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9DdXRzY2VuZXMvSW50cm9WaWRlbyIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0N1dHNjZW5lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA5OjE3OjM3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mU3BhaW4iLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiTGlzYm9uIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWRyaWQiLCJzdWNjZXNzIjoiVHJ1ZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNaWFtaSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkpvaG4gU21pdGgifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJPcmxhbmRvIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6IlRhbXBhIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSm9obiBTbWl0aCJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6IlRhbGxhaGFzc2VlIiwic3VjY2VzcyI6IlRydWUifSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxMzowNToxMloifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9pbml0aWFsaXplZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjA1OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDU6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0xIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuMX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjA1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjF9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC4zNX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjU1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNzV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTozNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9jb21wbGV0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IkZhbHNlIiwic2NvcmUiOjE3fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NDg6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiRmFsc2UiLCJzY29yZSI6MzN9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo1NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL1NjcmVlbnMvU291bmRNZW51IiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvU2NyZWVuIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9hY2Nlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvY3V0c2NlbmVzL0ludHJvVmlkZW8iLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9DdXRzY2VuZSJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJTYXJhaCBDb25ub3IifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9WaWRlb1R1dG9yaWFsIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vaWQudGluY2FuYXBpLmNvbS92ZXJiL3NraXBwZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDk6MTc6MzdaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZlNwYWluIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ikxpc2JvbiIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWxsb3JjYSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ik1pYW1pIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZkZsb3JpZGEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiT3JsYW5kbyIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IlNhcmFoIENvbm5vciJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6IlRhbXBhIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiU2FyYWggQ29ubm9yIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZkZsb3JpZGEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiSmFja3NvbnZpbGxlIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTIiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MDM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNDV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuOH19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC45OX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjM1OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9jb21wbGV0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IlRydWUiLCJzY29yZSI6OTV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTozNzoxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiVHJ1ZSIsInNjb3JlIjo5NX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjM3OjEzWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL1NjcmVlbnMvU291bmRNZW51IiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvU2NyZWVuIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9hY2Nlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvY3V0c2NlbmVzL1ZpZGVvVHV0b3JpYWwiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9DdXRzY2VuZSJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKYW1lcyBEZWFuIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vaWQudGluY2FuYXBpLmNvbS92ZXJiL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9DdXRzY2VuZXMvVmlkZW9EZW1vIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDk6MTc6MzdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSmFtZXMgRGVhbiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWRyaWQiLCJzdWNjZXNzIjoiVHJ1ZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNaWFtaSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6IkphbWVzIERlYW4ifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJUYWxsYWhhc3NlZSIsInN1Y2Nlc3MiOiJUcnVlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvaW5pdGlhbGl6ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNToyNTo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9pbml0aWFsaXplZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0yIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjA1OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC4yNX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjM2OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjM1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6Mzg6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuNjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0NTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC43NX19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjQ3OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0yIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiRmFsc2UiLCJzY29yZSI6NzJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0ODoxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9jb21wbGV0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL2V4dC92YWx1ZSI6IkdhbWUgT3ZlciIsInN1Y2Nlc3MiOiJUcnVlIiwic2NvcmUiOjcyfX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NTk6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9TY3JlZW5zL1NvdW5kTWVudSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL1NjcmVlbiJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQwNzo0Nzo0N1oifQp7ImFjdG9yIjp7Im5hbWUiOiJNYXJ5IEthdGUifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vaWQudGluY2FuYXBpLmNvbS92ZXJiL3NraXBwZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0N1dHNjZW5lcy9JbnRyb1ZpZGVvIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDk6MTc6MzdaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZlNwYWluIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ikxpc2JvbiIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6Ik1hcnkgS2F0ZSJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZGbG9yaWRhIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQWx0ZXJuYXRpdmUifX0sInJlc3VsdCI6eyJyZXNwb25zZSI6Ik1pYW1pIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiTWFyeSBLYXRlIn0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvc2VsZWN0ZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL0NhcGl0YWxzL0NhcGl0YWxPZkZsb3JpZGEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9zZXJpb3VzZ2FtZXMvYWN0aXZpdGllcy9BbHRlcm5hdGl2ZSJ9fSwicmVzdWx0Ijp7InJlc3BvbnNlIjoiSmFja3NvbnZpbGxlIiwic3VjY2VzcyI6IkZhbHNlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MTM6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTEiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MTU6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2luaXRpYWxpemVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTIiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL1N1cGVyTWFyaW9Ccm9zL0xldmVscy9Xb3JsZDEtMSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9sZXZlbCJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjV9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTowNTo0OVoifQp7ImFjdG9yIjp7Im5hbWUiOiJKdWFuIEdvbWV6In0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvcHJvZ3Jlc3NlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvTXlGaXJzdEdhbWUiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvc2VyaW91cy1nYW1lIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3czaWQub3JnL3hhcGkvc2VyaW91c2dhbWVzL2V4dGVuc2lvbnMvcHJvZ3Jlc3MiOjAuNSwicHJvZ3Jlc3MiOjAuM319LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDE1OjE3OjQ5WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9wcm9ncmVzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9NeUZpcnN0R2FtZSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHA6Ly9jdXJhdHIzLmNvbS9kZWZpbmUvdHlwZS9zZXJpb3VzLWdhbWUifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vdzNpZC5vcmcveGFwaS9zZXJpb3VzZ2FtZXMvZXh0ZW5zaW9ucy9wcm9ncmVzcyI6MC41LCJwcm9ncmVzcyI6MC41fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MjU6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjc1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6MzM6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3Byb2dyZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly93M2lkLm9yZy94YXBpL3NlcmlvdXNnYW1lcy9leHRlbnNpb25zL3Byb2dyZXNzIjowLjUsInByb2dyZXNzIjowLjk1fX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NDA6NDlaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL1dvcmxkMS0xIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL2xldmVsIn19LCJyZXN1bHQiOnsiZXh0ZW5zaW9ucyI6eyJodHRwczovL3JhZ2UuZS11Y20uZXMveGFwaS9leHQvdmFsdWUiOiJHYW1lIE92ZXIiLCJzdWNjZXNzIjoiRmFsc2UiLCJzY29yZSI6MjJ9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo1NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKdWFuIEdvbWV6In0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWRsbmV0Lmdvdi9leHBhcGkvdmVyYnMvY29tcGxldGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9TdXBlck1hcmlvQnJvcy9MZXZlbHMvV29ybGQxLTIiLCJkZWZpbml0aW9uIjp7InR5cGUiOiJodHRwOi8vY3VyYXRyMy5jb20vZGVmaW5lL3R5cGUvbGV2ZWwifX0sInJlc3VsdCI6eyJleHRlbnNpb25zIjp7Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL2V4dC92YWx1ZSI6IkdhbWUgT3ZlciIsInN1Y2Nlc3MiOiJUcnVlIiwic2NvcmUiOjYwfX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTU6NTU6MTNaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL2NvbXBsZXRlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvU3VwZXJNYXJpb0Jyb3MvTGV2ZWxzL015Rmlyc3RHYW1lIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cDovL2N1cmF0cjMuY29tL2RlZmluZS90eXBlL3NlcmlvdXMtZ2FtZSJ9fSwicmVzdWx0Ijp7ImV4dGVuc2lvbnMiOnsiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvZXh0L3ZhbHVlIjoiR2FtZSBPdmVyIiwic3VjY2VzcyI6IlRydWUiLCJzY29yZSI6NDN9fSwidGltZXN0YW1wIjoiMjAxNi0wNS0yNFQxNTo0NToxM1oifQp7ImFjdG9yIjp7Im5hbWUiOiJKdWFuIEdvbWV6In0sInZlcmIiOnsiaWQiOiJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL2FjY2Vzc2VkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9jdXRzY2VuZXMvSW50cm9WaWRlbyIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0N1dHNjZW5lIn19LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDA3OjQ3OjQ3WiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvYWNjZXNzZWQifSwib2JqZWN0Ijp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL2dhbWVzL2N1dHNjZW5lcy9WaWRlb1R1dG9yaWFsIiwiZGVmaW5pdGlvbiI6eyJ0eXBlIjoiaHR0cHM6Ly9yYWdlLmUtdWNtLmVzL3hhcGkvc2VyaW91c2dhbWVzL2FjdGl2aXRpZXMvQ3V0c2NlbmUifX0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMDc6NDc6NDdaIn0KeyJhY3RvciI6eyJuYW1lIjoiSnVhbiBHb21leiJ9LCJ2ZXJiIjp7ImlkIjoiaHR0cDovL2FkbG5ldC5nb3YvZXhwYXBpL3ZlcmJzL3NlbGVjdGVkIn0sIm9iamVjdCI6eyJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9nYW1lcy9DYXBpdGFscy9DYXBpdGFsT2ZTcGFpbiIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNYWRyaWQiLCJzdWNjZXNzIjoiVHJ1ZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJNaWFtaSIsInN1Y2Nlc3MiOiJGYWxzZSJ9LCJ0aW1lc3RhbXAiOiIyMDE2LTA1LTI0VDEzOjA1OjEyWiJ9CnsiYWN0b3IiOnsibmFtZSI6Ikp1YW4gR29tZXoifSwidmVyYiI6eyJpZCI6Imh0dHA6Ly9hZGxuZXQuZ292L2V4cGFwaS92ZXJicy9zZWxlY3RlZCJ9LCJvYmplY3QiOnsiaWQiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2FtZXMvQ2FwaXRhbHMvQ2FwaXRhbE9mRmxvcmlkYSIsImRlZmluaXRpb24iOnsidHlwZSI6Imh0dHBzOi8vcmFnZS5lLXVjbS5lcy94YXBpL3NlcmlvdXNnYW1lcy9hY3Rpdml0aWVzL0FsdGVybmF0aXZlIn19LCJyZXN1bHQiOnsicmVzcG9uc2UiOiJUYWxsYWhhc3NlZSIsInN1Y2Nlc3MiOiJUcnVlIn0sInRpbWVzdGFtcCI6IjIwMTYtMDUtMjRUMTM6MDU6MTJaIn0K']\n", - " list_of_names = ['xapi-sg-sample-data.json']\n", - " list_of_dates = [1715587109.421]\n", - " 56 zip(list_of_contents, list_of_names, list_of_dates)\n", - " 57 ]\n", - "\n", - "TypeError: load_players_info_from_content() takes 2 positional arguments but 3 were given\n", - "\n" - ] - } - ], - "source": [ - "from dash import Dash, html, dash_table, dcc, callback, Output, Input, State\n", - "import plotly.subplots as subplots\n", - "import fileBrowserAndUploadButtonToLoadProcessStatements\n", - "\n", - "global players_info\n", - "# Initialize the app\n", - "app = Dash(__name__)\n", - "\n", - "# App layout\n", - "app.layout = html.Div([\n", - " html.H1(children='T-Mon'),\n", - " html.Hr(),\n", - " html.H2(children='Select JSON xAPI-SG file to process and see visualisations'),\n", - " dcc.Upload(\n", - " id='upload-data',\n", - " children=html.Div([\n", - " 'Drag and Drop or ',\n", - " html.A('Select Files')\n", - " ]),\n", - " style={\n", - " 'width': '100%',\n", - " 'height': '60px',\n", - " 'lineHeight': '60px',\n", - " 'borderWidth': '1px',\n", - " 'borderStyle': 'dashed',\n", - " 'borderRadius': '5px',\n", - " 'textAlign': 'center',\n", - " 'margin': '10px'\n", - " },\n", - " # Allow multiple files to be uploaded\n", - " multiple=True\n", - " ),\n", - " html.Div(id='output-data-upload'),\n", - " html.Div(id='output-eror', style={'whiteSpace': 'pre-line'}),\n", - " html.Hr(),\n", - " dcc.Tabs(id=\"t-mon-tabs\", value='progress', children=[\n", - " dcc.Tab(label='Progress', value='progress_tab'),\n", - " dcc.Tab(label='Videos', value='video_tab'),\n", - " dcc.Tab(label='Completable', value='completable_tab'),\n", - " dcc.Tab(label='Alternatives', value='alternative_tab'),\n", - " dcc.Tab(label='Interactions', value='interaction_tab'),\n", - " dcc.Tab(label='Accessible', value='interaction_tab'),\n", - " dcc.Tab(label='Menu', value='menu_tab'),\n", - " ]),\n", - " html.H4(children='T-MON, by eUCM research team')\n", - "])\n", - "\n", - "@callback(Output('output-data-upload', 'children'),\n", - " Input('upload-data', 'contents'),\n", - " State('upload-data', 'filename'),\n", - " State('upload-data', 'last_modified'))\n", - "def update_output(list_of_contents, list_of_names, list_of_dates):\n", - " if list_of_contents is not None:\n", - " return [\n", - " fileBrowserAndUploadButtonToLoadProcessStatements.load_players_info_from_content(c, n, d) for c, n, d in\n", - " zip(list_of_contents, list_of_names, list_of_dates)\n", - " ]\n", - " \n", - "if __name__ == '__main__':\n", - " app.run(debug=True, port=8051)" - ] } ], "metadata": { diff --git a/requirements.txt b/requirements.txt index 6b4b95c..6f699ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,7 @@ requests boto3 jwt plotly -dash \ No newline at end of file +dash +dash-auth +flask +flask_oidc \ No newline at end of file diff --git a/vis/__init__.py b/vis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vis/helpersFunctions/HeatMap.ipynb b/vis/helpersFunctions/HeatMap.ipynb deleted file mode 100644 index 2f50e41..0000000 --- a/vis/helpersFunctions/HeatMap.ipynb +++ /dev/null @@ -1,132 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "***constructHeatMap* function** to constuct the multilist for the heatmap\n", - "\n", - "Input :\n", - "* players_info dict with players\n", - "* playersSelected : the selected players\n", - "* labels : labels for the construction\n", - "* action : the action to have to search in players_info\n", - "* typ : boolean if this action is divided in type or not\n", - "\n", - "Output:\n", - "* multiList: the list with the values for the HeatMap" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def constructHeatMap(players_info, playersSelected, labels, action, typ=False):\n", - " multiList=[]\n", - " i=0\n", - " for label in labels:\n", - " multiList.insert(i,[])\n", - " j=0\n", - " for player in playersSelected:\n", - " if typ:\n", - " nb=0\n", - " for type in players_info[player][action].keys():\n", - " if label in players_info[player][action][type].keys():\n", - " nb+=len(players_info[player][action][type][label])\n", - " multiList[i].insert(j,nb)\n", - " j+=1\n", - " else:\n", - " if label in players_info[player][action].keys():\n", - " multiList[i].insert(j,len(players_info[player][action][label]))\n", - " else:\n", - " multiList[i].insert(j,0)\n", - " j+=1\n", - " multiList[i]=list(multiList[i])\n", - " i+=1\n", - " return multiList" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***heatMap* function** to display the heatMap\n", - "\n", - "Input :\n", - "* multiList: the list with the values for the HeatMap\n", - "* ticksXlabel : labels for the ticks of X axe of the heatmap\n", - "* ticksYlabel : labels for the ticks of Y axe of the heatmap\n", - "* figTab : list of fig for this heatmap\n", - "* figtitle : title of the fig for save the heatmap\n", - "\n", - "Output: display the heatmap with a scale of color to the right" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def heatMap(multiTab, ticksXlabel, ticksYlabel, figTab, figtitle):\n", - " #create new fig with custom name in the tab\n", - " name=filename+figtitle\n", - " fig, ax = plt.subplots(num=name, constrained_layout=True, figsize=(len(ticksXlabel)/2, len(ticksYlabel)/2))\n", - " fig.canvas.header_visible = False\n", - " fig.canvas.layout.min_height = '400px'\n", - " figTab.append(fig)\n", - " im = ax.imshow(multiTab, cmap='cool', interpolation='nearest')\n", - " for i in range(len(ticksYlabel)):\n", - " for j in range(len(ticksXlabel)):\n", - " text = ax.text(j, i, multiTab[i][j],\n", - " ha=\"center\", va=\"center\", color=\"b\")\n", - " cbar=plt.colorbar(im)\n", - " # We want to show all ticks...\n", - " ax.set_xticks(np.arange(len(ticksXlabel)))\n", - " ax.set_yticks(np.arange(len(ticksYlabel)))\n", - " # ... and label them with the respective list entries\n", - " ax.set_xticklabels(ticksXlabel)\n", - " ax.set_yticklabels(ticksYlabel)\n", - " # Rotate the tick labels and set their alignment.\n", - " plt.setp(ax.get_xticklabels(), rotation=45, ha=\"right\",rotation_mode=\"anchor\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/vis/helpersFunctions/MultiBarMultiColor.ipynb b/vis/helpersFunctions/MultiBarMultiColor.ipynb deleted file mode 100644 index 69f5e9a..0000000 --- a/vis/helpersFunctions/MultiBarMultiColor.ipynb +++ /dev/null @@ -1,80 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": true, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "***constructDictWithKeyAndSubKey* function** to constuct the dictionary for the multibar\n", - "\n", - "Input :\n", - "* players_info : dict with players\n", - "* playersSelected : players selected\n", - "* keys : labels for the construction\n", - "* action : the action to have to search in players_info\n", - "* typ : boolean if this action is divided in type or not\n", - "\n", - "Output:\n", - "* dictionary: the dictionary with the values for the multibar" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def constructDictWithKeyAndSubKey(players_info, playersSelected, key, action, typ=False):\n", - " dictionary={key:{}}\n", - " for player in playersSelected:\n", - " if typ:\n", - " for type in players_info[player][action].keys():\n", - " if key in players_info[player][action][type].keys():\n", - " for sub_key in players_info[player][action][type][key]:\n", - " if not sub_key in dictionary[key].keys():\n", - " dictionary[key][sub_key]={}\n", - " if not player in dictionary[key][sub_key].keys():\n", - " dictionary[key][sub_key][player]=0\n", - " dictionary[key][sub_key][player]+=len(players_info[player][action][type][key][sub_key])\n", - " else:\n", - " for sub_key in players_info[player][action][key]:\n", - " if not sub_key in dictionary[key].keys():\n", - " dictionary[key][sub_key]={}\n", - " if not player in dictionary[key][sub_key].keys():\n", - " dictionary[key][sub_key][player]=0\n", - " dictionary[key][sub_key][player]+=len(players_info[player][action][key][sub_key])\n", - " return dictionary" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/vis/helpersFunctions/MultiBarSeparated.ipynb b/vis/helpersFunctions/MultiBarSeparated.ipynb deleted file mode 100644 index fe622ea..0000000 --- a/vis/helpersFunctions/MultiBarSeparated.ipynb +++ /dev/null @@ -1,180 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***constructMultiTab* function** to constuct the multilist for the heatmap\n", - "\n", - "Input :\n", - "* players_info : dict with players\n", - "* playersSelected : players selected\n", - "* labels : labels for the construction\n", - "* action : the action to have to search in players_info\n", - "* typ : boolean if this action is divided in type or not\n", - "\n", - "Output:\n", - "* dictionary: the dictionary with the values for the multibar" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def constructMultiTab(players_info, playersSelected, labels, action, typ=False):\n", - " dictionary={}\n", - " for label in labels:\n", - " dictionary[label]={}\n", - " for player in playersSelected:\n", - " if typ:\n", - " nb=0\n", - " for type in players_info[player][action].keys():\n", - " for key in players_info[player][action][type].keys():\n", - " if key==label:\n", - " nb+=len(players_info[player][action][type][label])\n", - " dictionary[label][player]=nb\n", - " else:\n", - " for key in players_info[player][action].keys():\n", - " if key==label:\n", - " dictionary[label][player]=len(players_info[player][action][label])\n", - " else:\n", - " dictionary[label][player]=0\n", - " return dictionary" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "***constructSimpleTab* function** to constuct the dict to the barchart\n", - "\n", - "Input :\n", - "* playersSelected : players selected\n", - "* label : label for the construction\n", - "* action : the action to have to search in players_info\n", - "* typ : boolean if this action is divided in type or not\n", - "\n", - "Output:\n", - "* dictionary: the dictionary with the values for the simplebar" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def constructSimpleTab(playersSelected, label, action, typ=False):\n", - " dictionary={}\n", - " for player in playersSelected:\n", - " if typ:\n", - " nb=0\n", - " for type in players_info[player][action].keys():\n", - " for key in players_info[player][action][type].keys():\n", - " if key==label:\n", - " nb+=len(players_info[player][action][type][label])\n", - " dictionary[player]=nb\n", - " else:\n", - " for key in players_info[player][action].keys():\n", - " if key==label:\n", - " dictionary[player]=len(players_info[player][action][label])\n", - " else:\n", - " dictionary[player]=0\n", - " return dictionary" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***multiBarSeparated* function** to display the multiBar chart\n", - "\n", - "Input :\n", - "* dictionary: the list with all the values\n", - "* order : selection to order values alphabeticly or per values\n", - "* title : title of the multibar graph\n", - "* xlabel : labels for the X axe\n", - "* ylabel : labels for the Y axe\n", - "* figtitle : title of the fig for save the barchart\n", - "\n", - "Output: display the multiBar chart" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%run vis/helpersFunctions/barCharts.ipynb\n", - "def multiBarSeparated(dictionary, order,title, xlabel, ylabel, figTab, filetitle):\n", - " multiBar={}\n", - " for key in dictionary.keys():\n", - " dictionary[key]=orderDict(dictionary[key],order)\n", - " multiBar[key]=widgets.Output()\n", - " #TODO only display the one selected in the accordion\n", - " with multiBar[key]:\n", - " #create new fig with custom name in the tab\n", - " name=name=filename+\"_\"+filetitle+\"_\"+key\n", - " fig = plt.figure(num=name, constrained_layout=True)\n", - " fig.canvas.header_visible = False\n", - " fig.canvas.layout.min_height = '400px'\n", - " figTab.append(fig)\n", - " displayEachBar(dictionary[key], key, title, xlabel, ylabel)\n", - " accordion=widgets.Accordion(list(multiBar.values()))\n", - " i=0\n", - " for key in multiBar.keys():\n", - " accordion.set_title(i, key)\n", - " i+=1\n", - " return tabs" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/vis/helpersFunctions/OrderGraphValuesAndSelectDataByXaxis.ipynb b/vis/helpersFunctions/OrderGraphValuesAndSelectDataByXaxis.ipynb deleted file mode 100644 index 9ebf972..0000000 --- a/vis/helpersFunctions/OrderGraphValuesAndSelectDataByXaxis.ipynb +++ /dev/null @@ -1,370 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "***separateDictToSubDictEachXvalues* function** get the dictionary and return a list of dictionaries, with for\n", - "each dictionary contain X elements separated from initial dictionary\n", - "Example : {a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9} and X=2=>[{a:1,b:2},{c:3,d:4},{e:5,f:6},{g:7,h:8},{i:9}]" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def separateDictToSubDictEachXvalues(dic, X):\n", - " item_dict=list(dic.items())\n", - " list_dict=[]\n", - " nbtimes=math.ceil(len(item_dict)/X)\n", - " for i in range(nbtimes):\n", - " list_dict.append(dict(item_dict[i*X:(i+1)*X]))\n", - " return list_dict" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***separateListToSubListEachXvalues* function** get the list and return a list of list, with for\n", - "each list contain X elements separated from list\n", - "Example : [1,2,3,4,5,6,7,8,9] and X=2=>[[1,2],[3,4},[5,6],[7,8],[9]] " - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def separateListToSubListEachXvalues(list, X):\n", - " list_list=[]\n", - " nbtimes=math.ceil(len(list)/X)\n", - " for i in range(nbtimes):\n", - " list_list.append(list[i*X:(i+1)*X])\n", - " return list_list\n" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***organizeDictPerxAxisSelected* function** to separate dict to have a certain data to see for the dict of the position of the bars\n", - "\n", - "Input :\n", - "* listOfKeysDict : the list of the keys of the dictionary\n", - "* xaxis : 0 if all data, -1 (-2) if 10 first (10 last), or 5, 25, 50 per visualisation for x axis\n", - "* dic : the dict to separated" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 3, - "outputs": [], - "source": [ - "nbFirstLast=10\n", - "def organizeDictPerxAxisSelected(listOfKeysDict, xaxis, dic):\n", - " N=len(listOfKeysDict)\n", - " nbgraphshown=1\n", - " if xaxis==0: #getAllDataInAListOfList\n", - " dic=[dic]\n", - " elif xaxis==-1: #getnbFirstData\n", - " dic=[dict(list(dic.items())[:nbFirstLast])]\n", - " if N>nbFirstLast:\n", - " N=nbFirstLast\n", - " elif xaxis==-2: #getnbLastData\n", - " dic=[dict(list(dic.items())[-nbFirstLast:])]\n", - " if N>nbFirstLast:\n", - " N=nbFirstLast\n", - " else: #getxaxisDataSeparatedInListOfLists\n", - " dic=separateDictToSubDictEachXvalues(dic,xaxis)\n", - " N=xaxis\n", - " nbgraphshown=len(dic)\n", - " return nbgraphshown, dic, N" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***organizeDictPosPerDataLabels* function** to separate dict to have a certain data to see for the dict of the position of the bars\n", - "\n", - "Input :\n", - "* xaxis : 0 if all data, -1 (-2) if 10 first (10 last), or 5, 25, 50 per visualisation for x axis\n", - "* pos : the dict to separated" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 4, - "outputs": [], - "source": [ - "def organizeDictPosPerDataLabels(xaxis, pos):\n", - " if xaxis==0: #getAllDataInAListOfList\n", - " return [pos]\n", - " elif xaxis==-1: #getnbFirstData\n", - " return [dict(list(pos.items())[:nbFirstLast])]\n", - " elif xaxis==-2: #getnbLastData\n", - " return [dict(list(pos.items())[-nbFirstLast:])]\n", - " else: #getxaxisDataSeparatedInListOfLists\n", - " return separateDictToSubDictEachXvalues(pos, xaxis)\n", - "\n" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***organizeDictPosPerDataxaxisDiff* function** to separate dict to have a certain data to see for the dict of the position of the bars\n", - "\n", - "Input :\n", - "* xaxis : 0 if all data, -1 (-2) if 10 first (10 last), or 5, 25, 50 per visualisation for x axis\n", - "* pos : the dict to separated" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def organizeDictPosPerDataxaxis(xaxis, pos):\n", - " pos2={}\n", - " for key in pos.keys():\n", - " if xaxis==0: #getAllDataInAListOfList\n", - " pos2[key]=[pos[key]]\n", - " elif xaxis==-1: #getnbFirstData\n", - " pos2[key]=[pos[key][:nbFirstLast]]\n", - " elif xaxis==-2: #getnbLastData\n", - " pos2[key]=[pos[key][-nbFirstLast:]]\n", - " else:\n", - " pos2[key]=separateListToSubListEachXvalues(pos[key], xaxis)\n", - " return pos2" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***reorganiseDictBySubKeys* function** reorganize the dict by subkeys and keys of the original dict\n", - "\n", - "Input :\n", - "* dict : the dict to reorganize" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 5, - "outputs": [], - "source": [ - "def reorganiseDictBySubKeys(dict):\n", - " temp_dict={}\n", - " for key in dict.keys():\n", - " for subkey in dict[key].keys():\n", - " if not subkey in temp_dict.keys():\n", - " temp_dict[subkey]={}\n", - " temp_dict[subkey][key]=dict[key][subkey]\n", - " return temp_dict" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***getPosDictMultiBarChart* function** construct the dict for the positions of the bars\n", - "\n", - "Input :\n", - "* ordered_values : the dict of ordered values\n", - "* listOfSubkeys : the list with the labels of the subkeys of the dict to know how many bar we need to have\n", - "* N : number of values per keys" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": 6, - "outputs": [], - "source": [ - "def getPosDictMultiBarChart(ordered_values, listOfSubkeys, N=None):\n", - " dict={}\n", - " for key in ordered_values.keys():\n", - " dict[key]={}\n", - " i=0\n", - " for subkey in ordered_values[key].keys():\n", - " dict[key][subkey]=i\n", - " i+=1\n", - " nbSubKey=len(listOfSubkeys)\n", - " if nbSubKey>0 and len(ordered_values.keys())>0:\n", - " bar_width = 1.0/nbSubKey\n", - " else:\n", - " return {}, 0\n", - " pos={}\n", - " nkey=0\n", - " for key in ordered_values.keys():\n", - " for subkey in ordered_values[key].keys():\n", - " if not subkey in pos.keys():\n", - " pos[subkey]=[]\n", - " pos[subkey].append(nkey+dict[key][subkey]*bar_width)\n", - " nkey+=1\n", - " if nkey==N:\n", - " nkey=0\n", - " return pos, bar_width" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***orderDict* function** to order all the dictionary values to order the values\n", - "\n", - "Input :\n", - "* dictNotOrdered : the dict to order\n", - "* order : the selected order (1: min to max, -1 : max to min, 0 : alphabetical)" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 7, - "outputs": [], - "source": [ - "def orderDictByDictValues(dictNotOrdered,order):\n", - " ordered={}\n", - " if order==1: #value from highest to lowest\n", - " for key in dictNotOrdered.keys():\n", - " ordered[key]=dict(sorted(dictNotOrdered[key].items(), key=lambda item:item[1], reverse =True))\n", - " elif order==-1: #value from lowest to highest\n", - " for key in dictNotOrdered.keys():\n", - " ordered[key]=dict(sorted(dictNotOrdered[key].items(), key=lambda item:item[1]))\n", - " else: #alphabetical order\n", - " for key in dictNotOrdered.keys():\n", - " ordered[key]=dict(sorted(dictNotOrdered[key].items(), key=lambda item:item[0]))\n", - " return ordered" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***orderDict* function** to order the dictionary with a lambda function to order the values\n", - "\n", - "Input :\n", - "* dictNotOrdered : the dict to order\n", - "* order : the selected order (1: min to max, -1 : max to min, 0 : alphabetical)\n", - "* lambdafunctionForValueOrder : the lambda function to order the values by hight to lox (or reverse)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": 8, - "outputs": [], - "source": [ - "def orderDict(dictNotOrdered,order,lambdafunctionForValueOrder=(lambda item:item[1])):\n", - " ordered={}\n", - " if order==1: #value from highest to lowest\n", - " ordered=dict(sorted(dictNotOrdered.items(), key=lambdafunctionForValueOrder, reverse =True))\n", - " elif order==-1: #value from lowest to highest\n", - " ordered=dict(sorted(dictNotOrdered.items(), key=lambdafunctionForValueOrder))\n", - " else: #alphabetical order\n", - " ordered=dict(sorted(dictNotOrdered.items(), key=lambda item:item[0]))\n", - " return ordered" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/vis/helpersFunctions/TimeLineChart.ipynb b/vis/helpersFunctions/TimeLineChart.ipynb deleted file mode 100644 index a0aa376..0000000 --- a/vis/helpersFunctions/TimeLineChart.ipynb +++ /dev/null @@ -1,57 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "***absoluteToRelativeTime* function** give the relative time compare to the first value time\n", - "* Input : timeList > list of time\n", - "* Output : relativeTimeList > list of relative time in minutes" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "\n", - "def absoluteToRelativeTime(timeList):\n", - " startTime=timeList[0]\n", - " relativeTimeList=[]\n", - " for time in timeList:\n", - " t=time-startTime\n", - " relativeTimeList.append(t.total_seconds()/60.0)\n", - " return relativeTimeList" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/vis/helpersFunctions/barCharts.ipynb b/vis/helpersFunctions/barCharts.ipynb deleted file mode 100644 index 2130637..0000000 --- a/vis/helpersFunctions/barCharts.ipynb +++ /dev/null @@ -1,406 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "***get_two_superposed_values_bars* function** to construct a superposed bar chart\n", - "\n", - "Input :\n", - "* ind : the indicator to know where display the superposed bars\n", - "* list_xticks_labels : the list of x_ticks_labels to display\n", - "* listUp : the list of values to display up\n", - "* labelUp : the labels corresponding of the values to the listUp\n", - "* colorUp : the color to the up bars\n", - "* listDown : the list of values to display down\n", - "* labelDown : the labels corresponding of the values to the listDown\n", - "* colorDown : the color to the down bars" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def get_two_superposed_values_bars(ind, list_xticks_labels, listUp, labelUp, colorUp, listDown, labelDown, colorDown):\n", - " width = 0.35\n", - " maxValue=0\n", - " #get max value\n", - " for i in range(len(list_xticks_labels)):\n", - " temp=listUp[i]+listDown[i]\n", - " if temp>maxValue:\n", - " maxValue=temp\n", - " maxValue+=0.5\n", - " if not all_list_nbrep_empty(listUp):\n", - " if all_list_nbrep_empty(listDown):\n", - " #listUp only\n", - " p = plt.bar(ind, listUp, width, color=colorUp, label=labelUp)\n", - " add_value_labels(p)\n", - " else:\n", - " #the two superposed list\n", - " p1 = plt.bar(ind, listDown, width, color=colorDown, label=labelDown)\n", - " p2 = plt.bar(ind, listUp, width, color=colorUp, label=labelUp,\n", - " bottom=listDown)\n", - " add_value_labels(p1, [0 for i in listDown])\n", - " add_value_labels(p2, listDown)\n", - " else:\n", - " #listDown only\n", - " p = plt.bar(ind, listDown, width, color=colorDown, label=labelDown)\n", - " add_value_labels(p)\n", - " plt.xticks(ind, list_xticks_labels)\n", - " plt.ylim(0, maxValue)\n", - " plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***all_list_nbrep_empty* function** to see if one of the value of the list is 0\n", - "Input:\n", - "* list : the list to analyse\n", - "\n", - "Output:\n", - "* return : boolean > True if any value is equal to 0, false otherwise" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def all_list_nbrep_empty(list):\n", - " for nbrep in list:\n", - " if nbrep!=0:\n", - " return False\n", - " return True" - ] - }, - { - "cell_type": "markdown", - "source": [ - "***get_percentage_bar* function** to construct the bar chart in percentage\n", - "* list_values_sup_inf : the list of the values to calculate the percentage of those values\n", - "* list_values_inf : the list of the others values to calculate the percentage\n", - "* object_list : list of objects" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def get_percentage_bar(list_values_sup_inf, list_values_inf, object_list):\n", - " N = len(object_list)\n", - " ind = np.arange(N)\n", - " width = 0.35\n", - " percentages=[]\n", - " for (x,y) in zip(list_values_sup_inf,list_values_inf):\n", - " try:\n", - " #calculate percentage\n", - " pourcent=x/(x+y)*100\n", - " except ZeroDivisionError:\n", - " pourcent=0\n", - " percentages.append(pourcent)\n", - " #display percentage bar\n", - " bar=plt.bar(ind, percentages, width, color='b')\n", - " plt.xticks(ind, object_list, rotation=90)\n", - " plt.ylim(0,100)\n", - " # Call the function above. All the magic happens there.\n", - " add_value_labels(bar)" - ] - }, - { - "cell_type": "markdown", - "source": [ - "***add_value_labels* function** to add labels to the end of each bar in a bar chart.\n", - "\n", - "Arguments:\n", - "* ax (matplotlib.axes.Axes): The matplotlib object containing the axes of the plot to annotate.\n", - "* bottom : the list to display correcly the values when multibars are superposed\n", - "* spacing (int): The distance between the labels and the bars." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def add_value_labels(ax, bottom=[], spacing=5):\n", - " # For each bar: Place a label\n", - " i=0\n", - " for rect in ax:\n", - " # Get X and Y placement of label from rect.\n", - " if bottom==[]:\n", - " y_value = rect.get_height()\n", - " else:\n", - " y_value=bottom[i] + (rect.get_height() / 2)\n", - " value=rect.get_height()\n", - " x_value = rect.get_x() + rect.get_width() / 2\n", - "\n", - " # Number of points between bar and label. Change to your liking.\n", - " space = spacing\n", - " # Vertical alignment for positive values\n", - " va = 'bottom'\n", - "\n", - " # If value of bar is negative: Place label below bar\n", - " if y_value < 0:\n", - " # Invert space to place label below\n", - " space *= -1\n", - " # Vertically align label at top\n", - " va = 'top'\n", - "\n", - " # Use Y value as label and format number with one decimal place\n", - " label = \"{:.1f}\".format(y_value)\n", - "\n", - " # Create annotation\n", - " if bottom==[]:\n", - " plt.text(x_value, y_value, # Place label at end of the bar\n", - " label, # Use `label` as label\n", - " ha='center', #align to the center \n", - " va=va) # Vertically align label differently for\n", - " # # positive and negative values.\n", - " else:\n", - " if value!=0:\n", - " plt.text(x_value, y_value, # Place label at end of the bar\n", - " '%d' % int(value), # Use `label` as label\n", - " ha='center') #align to the center\n", - " i+=1" - ] - }, - { - "cell_type": "markdown", - "source": [ - "***constructMultiBarWithVerticalLines* function** to construct to multi bars visualisations with vertical lines and centered labels\n", - "\n", - "Input :\n", - "* dict : the dict containing all values to display\n", - "* n: the number of the graph to display\n", - "* listkey : the list of the keys to display correctly the xtickslabels\n", - "* listsubkeys : the list of the subkeys to display in x axe\n", - "* pos : a tab with all positions of the bars organize by keys and the number of the graph to display\n", - "* bar_width : the width of the bars\n", - "* N : number of total multibars" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def constructMultiBarWithVerticalLines(dict, n, listkey, listsubkeys, pos, bar_width, N):\n", - " maxValue=0\n", - " nkey=0\n", - " for key in dict[n].keys():\n", - " #get max value\n", - " for val in dict[n][key].values():\n", - " if val>maxValue:\n", - " maxValue=val\n", - " #add verticals lines\n", - " for i in range(len(dict[n][key])):\n", - " if i!=0:\n", - " plt.axvline(x=i-bar_width/2)\n", - " #add bars for key\n", - " bar=plt.bar([val-n*N for val in pos[key][n]],\n", - " list(dict[n][key].values()),\n", - " bar_width,\n", - " label=key)\n", - " ticks_label=list(dict[n][key].keys())\n", - " add_value_labels(bar)\n", - " nkey+=1\n", - " \n", - " # add ~20% more for aesthetics\n", - " if maxValue < 1.2:\n", - " maxValue += .2\n", - " elif maxValue < 5:\n", - " maxValue += 1\n", - " else:\n", - " while (maxValue%5) != 0: maxValue += 1 # ensure roundness\n", - " maxValue+=int(maxValue*.2)\n", - " \n", - " #set value and position for xticks\n", - " plt.gca().axes.set_xticks([x+(bar_width*len(listkey)/2) - bar_width/2 for x in range(len(ticks_label))])\n", - " plt.gca().axes.set_xticklabels(ticks_label)\n", - " plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", - " plt.xticks(rotation=90)\n", - " plt.ylim((0,maxValue))\n", - " if N>len(listsubkeys):\n", - " xlmax=len(listsubkeys)\n", - " else:\n", - " xlmax=N\n", - " plt.xlim((-bar_width,N))" - ] - }, - { - "cell_type": "markdown", - "source": [ - "***multiBarSuperposed* function** to construct superposed bars visualisations with centered labels\n", - "\n", - "Input :\n", - "* dictionary : the dict containing all values to display\n", - "* key: the label value\n", - "* title : title of the multibar graph\n", - "* xlabel : labels for the X axe\n", - "* ylabel : labels for the Y axe" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def multiBarSuperposed(dictionary, key, title, xlabel, ylabel):\n", - " j=0\n", - " for sub_key in dictionary.keys():\n", - " if j==0:\n", - " botom=[0 for i in range(0, len(dictionary[sub_key]))]\n", - " bar=plt.bar(range(len(dictionary[sub_key])),\n", - " dictionary[sub_key].values(),\n", - " label=sub_key,\n", - " tick_label=list(dictionary[sub_key].keys()))\n", - " add_value_labels(bar, botom)\n", - " botom=list(dictionary[sub_key].values())\n", - " else:\n", - " bar=plt.bar(range(len(dictionary[sub_key])),\n", - " dictionary[sub_key].values(),\n", - " label=sub_key,\n", - " tick_label=list(dictionary[sub_key].keys()),\n", - " bottom=botom)\n", - " add_value_labels(bar, botom)\n", - " temp=botom\n", - " l=list(dictionary[sub_key].values())\n", - " botom=[temp[i]+l[i] for i in range(0,len(l))]\n", - " j+=1\n", - " plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", - " plt.xticks(rotation=90)\n", - " plt.title(title+key)\n", - " plt.xlabel(xlabel)\n", - " plt.ylabel(ylabel)\n", - " plt.show()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***displayEachBar* function** to construct simple bars visualisations with centered labels\n", - "\n", - "Input :\n", - "* dictionary : the dict containing all values to display\n", - "* key: the label value\n", - "* title : title of the multibar graph\n", - "* xlabel : labels for the X axe\n", - "* ylabel : labels for the Y axe" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def displayEachBar(dictionary, key, title, xlabel, ylabel):\n", - " max=0\n", - " for val in dictionary.values():\n", - " if val>max:\n", - " max=val\n", - " max+=1\n", - " bar=plt.bar(range(len(dictionary)),\n", - " dictionary.values(),\n", - " tick_label=list(dictionary.keys()))\n", - " add_value_labels(bar)\n", - " plt.title(title + key)\n", - " plt.xlabel(xlabel)\n", - " plt.ylabel(ylabel)\n", - " plt.xticks(rotation=90)\n", - " plt.ylim(0,max)\n", - " plt.show()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} \ No newline at end of file diff --git a/vis/helpersFunctions/bubbleChartFunctionTime.ipynb b/vis/helpersFunctions/bubbleChartFunctionTime.ipynb deleted file mode 100644 index 553565e..0000000 --- a/vis/helpersFunctions/bubbleChartFunctionTime.ipynb +++ /dev/null @@ -1,100 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "***constructAndDisplayBubbleChart* function**:\n", - "* Input: players_times_action dictionary and actions : list of actions\n", - "* Display a bubble chart showing a bubble in function of the average\n", - "of players has done this action in a certain period of time." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def constructAndDisplayBubbleChart(actions, players_times_action):\n", - " player_action={}\n", - " players_times_number_of_players={}\n", - " for action in players_times_action.keys():\n", - " #the dict players_times_action is order per action, and for each action per time (key) with value the player\n", - " #take an delta time of 1 minute to chack if we have much action by all players\n", - " dtmax=timedelta(minutes = 1)\n", - " dt=0\n", - " i=0\n", - " presentplayer=[]\n", - " for time in players_times_action[action].keys():\n", - " if i==0:\n", - " #first point\n", - " time0=time\n", - " presentplayer.append(players_times_action[action][time])\n", - " timemoy=time0\n", - " else:\n", - " #nexts times\n", - " time1=time\n", - " #delta time\n", - " dt=time1-time0\n", - " #if dt>1 minute\n", - " if dt>dtmax:\n", - " #add last point to the display\n", - " player_action[timemoy]=action\n", - " players_times_number_of_players[timemoy]=len(presentplayer)\n", - " #new point\n", - " time0=time\n", - " presentplayer=[players_times_action[action][time]]\n", - " timemoy=time0\n", - " else:\n", - " #update point\n", - " dt2=dt*(1/2)\n", - " #take player\n", - " player=players_times_action[action][time]\n", - " #add player to list of present player to all only 1 time if it do the action multiple times\n", - " if player not in presentplayer :\n", - " presentplayer.append(player)\n", - " #update timemoy\n", - " timemoy=time0+dt2\n", - " i+=1\n", - " #display scatter bubble graph\n", - " x = list(player_action.keys())\n", - " y = list(player_action.values())\n", - " z = list(players_times_number_of_players.values())\n", - " plt.scatter(x,y,z)\n", - " plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S %d-%m-%y'))\n", - " plt.gca().xaxis.set_major_locator(mdates.MinuteLocator(interval=10))\n", - " plt.xticks(rotation=45, ha=\"right\")\n", - " plt.yticks(fontsize=5)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/vis/helpersFunctions/clearFigMatplotlib.ipynb b/vis/helpersFunctions/clearFigMatplotlib.ipynb deleted file mode 100644 index 7333189..0000000 --- a/vis/helpersFunctions/clearFigMatplotlib.ipynb +++ /dev/null @@ -1,408 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Disable auto-scrolling of long output" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%%javascript\n", - "IPython.OutputArea.auto_scroll_threshold = 9999;" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Enabling the matplotlib widgets for Jupyter Notebook" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "! jupyter server extension enable --py --sys-prefix widgetsnbextension > /dev/null 2>&1;\n", - "! jupyter server extension enable ipympl --py --sys-prefix > /dev/null 2>&1;" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Create all outputs for the figs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "global outputGameStartCompleted,outputVideo, outputPlayerProgress\n", - "outputGameStartCompleted=widgets.Output()\n", - "outputVideo=widgets.Output()\n", - "outputPlayerProgress=widgets.Output()\n", - "\n", - "global outputCompletableProgress, outputCompletableScore, outputCompletableTime, outputCompletableProgressIncreaseDecrease\n", - "outputCompletableProgress=widgets.Output()\n", - "outputCompletableProgressIncreaseDecrease=widgets.Output()\n", - "outputCompletableScore=widgets.Output()\n", - "outputCompletableTime=widgets.Output()\n", - "\n", - "global outputAltPerPlayer,outputResPerAlt, outputAlternativeQuestion\n", - "outputAltPerPlayer=widgets.Output()\n", - "outputResPerAlt=widgets.Output()\n", - "outputAlternativeQuestion=widgets.Output()\n", - "\n", - "global outputBarChartItem, outputHeatMapItem, outputBubbleChartItem,outputActionTypeItem\n", - "outputBarChartItem=widgets.Output()\n", - "outputHeatMapItem=widgets.Output()\n", - "outputBubbleChartItem=widgets.Output()\n", - "outputActionTypeItem=widgets.Output()\n", - "\n", - "global outputBarChartAccessible, outputHeatMapAccessible, outputBubbleChartAccessible\n", - "outputBarChartAccessible=widgets.Output()\n", - "outputHeatMapAccessible=widgets.Output()\n", - "outputBubbleChartAccessible=widgets.Output()\n", - "\n", - "global outputMenu\n", - "outputMenu=widgets.Output()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Create all outputs for the update message" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "global outputGameStartCompletedUpdate,outputVideoUpdate, outputPlayerProgressUpdate\n", - "outputGameStartCompletedUpdate=widgets.Output()\n", - "outputVideoUpdate=widgets.Output()\n", - "outputPlayerProgressUpdate=widgets.Output()\n", - "\n", - "global outputCompletableProgressUpdate, outputCompletableScoreUpdate, outputCompletableTimeUpdate, \\\n", - " outputCompletableProgressIncreaseDecreaseUpdate\n", - "outputCompletableProgressUpdate=widgets.Output()\n", - "outputCompletableScoreUpdate=widgets.Output()\n", - "outputCompletableTimeUpdate=widgets.Output()\n", - "outputCompletableProgressIncreaseDecreaseUpdate=widgets.Output()\n", - "\n", - "global outputAltPerPlayerUpdate,outputResPerAltUpdate, outputAlternativeQuestionUpdate\n", - "outputAltPerPlayerUpdate=widgets.Output()\n", - "outputResPerAltUpdate=widgets.Output()\n", - "outputAlternativeQuestionUpdate=widgets.Output()\n", - "\n", - "global outputBarChartItemUpdate, outputHeatMapItemUpdate, outputBubbleChartItemUpdate,outputActionTypeItemUpdate\n", - "outputBarChartItemUpdate=widgets.Output()\n", - "outputHeatMapItemUpdate=widgets.Output()\n", - "outputBubbleChartItemUpdate=widgets.Output()\n", - "outputActionTypeItemUpdate=widgets.Output()\n", - "\n", - "global outputBarChartAccessibleUpdate, outputHeatMapAccessibleUpdate, outputBubbleChartAccessibleUpdate\n", - "outputBarChartAccessibleUpdate=widgets.Output()\n", - "outputHeatMapAccessibleUpdate=widgets.Output()\n", - "outputBubbleChartAccessibleUpdate=widgets.Output()\n", - "\n", - "global outputMenuUpdate\n", - "outputMenuUpdate=widgets.Output()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Create all list for figs of matplotlib widgets" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "global figGameStartedCompleted, figVideoSkipped, figPlayersProgress\n", - "figGameStartedCompleted=[]\n", - "figVideoSkipped=[]\n", - "figPlayersProgress=[]\n", - "\n", - "global figCompletableScore, figCompletableProgress, figCompletableTime, figCompletableProgressIncreaseDecrease\n", - "figCompletableScore=[]\n", - "figCompletableProgress=[]\n", - "figCompletableTime=[]\n", - "figCompletableProgressIncreaseDecrease=[]\n", - "\n", - "global figAltQuestion, figAltPlayer, figResPerAlt\n", - "figAltQuestion=[]\n", - "figAltPlayer=[]\n", - "figResPerAlt=[]\n", - "\n", - "global figBubbleChartItem, figHeatMapItem, figBarChartItem, figActionTypeItem\n", - "figBubbleChartItem=[]\n", - "figHeatMapItem=[]\n", - "figBarChartItem=[]\n", - "figActionTypeItem=[]\n", - "\n", - "global figBubbleChartAccessible, figHeatMapAccessible, figBarChartAccessible\n", - "figBubbleChartAccessible=[]\n", - "figHeatMapAccessible=[]\n", - "figBarChartAccessible=[]\n", - "\n", - "global figBarChartMenu\n", - "figBarChartMenu=[]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Clear and close figs" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def closeFigGameSelected():\n", - " #close and clear previous fig of gameStartAndComplete\n", - " for fig in figGameStartedCompleted:\n", - " plt.close(fig=fig)\n", - " figGameStartedCompleted.clear()\n", - " with outputGameStartCompleted:\n", - " clear_output(wait=False)\n", - " with outputGameStartCompletedUpdate:\n", - " print(\"Updating visualisation with the current selection of players...\")\n", - "\n", - "def closeFigVideos():\n", - " #close and clear previous fig of videos\n", - " for fig in figVideoSkipped:\n", - " plt.close(fig=fig)\n", - " figVideoSkipped.clear()\n", - " with outputVideo:\n", - " clear_output(wait=False)\n", - " with outputVideoUpdate:\n", - " print(\"Updating visualisation with the current selection of players and videos...\")\n", - "\n", - "def closeFigProgress():\n", - " #close and clear previous fig of progress players\n", - " for fig in figPlayersProgress:\n", - " plt.close(fig=fig)\n", - " figPlayersProgress.clear()\n", - " with outputPlayerProgress:\n", - " clear_output(wait=False)\n", - " with outputPlayerProgressUpdate:\n", - " print(\"Updating visualisation with the current selection of players...\")\n", - "\n", - "def closeFigCompletable():\n", - " #close and clear previous fig of completable progress\n", - " for fig in figCompletableProgress:\n", - " plt.close(fig=fig)\n", - " figCompletableProgress.clear()\n", - " with outputCompletableProgress:\n", - " clear_output(wait=False)\n", - " with outputCompletableProgressUpdate:\n", - " print(\"Updating visualisation with the current selection of players and completables...\")\n", - " #close and clear previous fig of completable progress increase/decrease\n", - " for fig in figCompletableProgressIncreaseDecrease:\n", - " plt.close(fig=fig)\n", - " figCompletableProgressIncreaseDecrease.clear()\n", - " with outputCompletableProgressIncreaseDecrease:\n", - " clear_output(wait=False)\n", - " with outputCompletableProgressIncreaseDecreaseUpdate:\n", - " print(\"Updating visualisation with the current selection of players and completables...\")\n", - " #close and clear previous fig of completable score\n", - " for fig in figCompletableScore:\n", - " plt.close(fig=fig)\n", - " figCompletableScore.clear()\n", - " with outputCompletableScore:\n", - " clear_output(wait=False)\n", - " with outputCompletableScoreUpdate:\n", - " print(\"Updating visualisation with the current selection of players and completables...\")\n", - " #close and clear previous fig of completable time\n", - " for fig in figCompletableTime:\n", - " plt.close(fig=fig)\n", - " figCompletableTime.clear()\n", - " with outputCompletableTime:\n", - " clear_output(wait=False)\n", - " with outputCompletableTimeUpdate:\n", - " print(\"Updating visualisation with the current selection of players and completables...\")\n", - "\n", - "def closeFigAlternative():\n", - " #close and clear previous fig of alternatives\n", - " for fig in figResPerAlt:\n", - " plt.close(fig=fig)\n", - " figResPerAlt.clear()\n", - " with outputResPerAlt:\n", - " clear_output(wait=False)\n", - " with outputResPerAltUpdate:\n", - " print(\"Updating visualisation with the current selection of players and alternatives...\")\n", - " for fig in figAltPlayer:\n", - " plt.close(fig=fig)\n", - " figAltPlayer.clear()\n", - " with outputAltPerPlayer:\n", - " clear_output(wait=False)\n", - " with outputAltPerPlayerUpdate:\n", - " print(\"Updating visualisation with the current selection of players and alternatives...\")\n", - " for fig in figAltQuestion:\n", - " plt.close(fig=fig)\n", - " figAltQuestion.clear()\n", - " with outputAlternativeQuestion:\n", - " clear_output(wait=False)\n", - " with outputAlternativeQuestionUpdate:\n", - " print(\"Updating visualisation with the current selection of players and alternatives...\")\n", - "\n", - "def closeFigInteractions():\n", - " #close and clear previous fig of items interactions\n", - " for fig in figBarChartItem:\n", - " plt.close(fig=fig)\n", - " figBarChartItem.clear()\n", - " with outputBarChartItem:\n", - " clear_output(wait=False)\n", - " with outputBarChartItemUpdate:\n", - " print(\"Updating visualisation with the current selection of players and items...\")\n", - " for fig in figHeatMapItem:\n", - " plt.close(fig=fig)\n", - " figHeatMapItem.clear()\n", - " with outputHeatMapItem:\n", - " clear_output(wait=False)\n", - " with outputHeatMapItemUpdate:\n", - " print(\"Updating visualisation with the current selection of players and items...\")\n", - " for fig in figBubbleChartItem:\n", - " plt.close(fig=fig)\n", - " figBubbleChartItem.clear()\n", - " with outputBubbleChartItem:\n", - " clear_output(wait=False)\n", - " with outputBubbleChartItemUpdate:\n", - " print(\"Updating visualisation with the current selection of players and items...\")\n", - " #close and clear previous fig of actions type\n", - " for fig in figActionTypeItem:\n", - " plt.close(fig=fig)\n", - " figActionTypeItem.clear()\n", - " with outputActionTypeItem:\n", - " clear_output(wait=False)\n", - " with outputActionTypeItemUpdate:\n", - " print(\"Updating visualisation with the current selection of players and items...\")\n", - "\n", - "def closeFigAccessibles():\n", - " #close and clear previous fig of accessible\n", - " for fig in figBarChartAccessible:\n", - " plt.close(fig=fig)\n", - " figBarChartAccessible.clear()\n", - " with outputBarChartAccessible:\n", - " clear_output(wait=False)\n", - " with outputBarChartAccessibleUpdate:\n", - " print(\"Updating visualisation with the current selection of players and accessibles...\")\n", - " for fig in figHeatMapAccessible:\n", - " plt.close(fig=fig)\n", - " figHeatMapAccessible.clear()\n", - " with outputHeatMapAccessible:\n", - " clear_output(wait=False)\n", - " with outputHeatMapAccessibleUpdate:\n", - " print(\"Updating visualisation with the current selection of players and accessibles...\")\n", - " for fig in figBubbleChartAccessible:\n", - " plt.close(fig=fig)\n", - " figBubbleChartAccessible.clear()\n", - " with outputBubbleChartAccessible:\n", - " clear_output(wait=False)\n", - " with outputBubbleChartAccessibleUpdate:\n", - " print(\"Updating visualisation with the current selection of players and accessibles...\")\n", - "\n", - "def closeFigMenus():\n", - " #close and clear previous fig of menus\n", - " for fig in figBarChartMenu:\n", - " plt.close(fig=fig)\n", - " figBarChartMenu.clear()\n", - " with outputMenu:\n", - " clear_output(wait=False)\n", - " with outputMenuUpdate:\n", - " print(\"Updating visualisation with the current selection of players and menus...\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/vis/helpersFunctions/getterObjectsList.ipynb b/vis/helpersFunctions/getterObjectsList.ipynb deleted file mode 100644 index 133d17e..0000000 --- a/vis/helpersFunctions/getterObjectsList.ipynb +++ /dev/null @@ -1,152 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": true, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "***get_full_list_of_objects* function** to get the full list of objects of the action selected.\n", - "\n", - "Input:\n", - "* action : the action to get in the players_info\n", - "* typ : if it is organise per type, get all per types, else union directly\n", - "* action2 : the 2nd action (facultative) to get in the players_info\n", - "* dict : True if it is organise in dictionary, else if in list" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def get_full_list_of_objects(action, typ=False, action2=None, dict=True):\n", - " # find full list of objects\n", - " objects_list = set()\n", - " for player in players_info.keys():\n", - " if typ:\n", - " for type in players_info[player][action].keys():\n", - " if dict:\n", - " objects_list = objects_list.union(players_info[player][action][type].keys())\n", - " else:\n", - " objects_list = objects_list.union(players_info[player][action][type])\n", - " if not action2==None:\n", - " for type in players_info[player][action2].keys():\n", - " if dict:\n", - " objects_list = objects_list.union(players_info[player][action2][type].keys())\n", - " else:\n", - " objects_list = objects_list.union(players_info[player][action2][type])\n", - " else:\n", - " if dict:\n", - " objects_list = objects_list.union(players_info[player][action].keys())\n", - " else:\n", - " objects_list = objects_list.union(players_info[player][action])\n", - " if not action2==None:\n", - " if dict:\n", - " objects_list = objects_list.union(players_info[player][action2].keys())\n", - " else:\n", - " objects_list = objects_list.union(players_info[player][action2])\n", - " objects_list = list(objects_list)\n", - " return sorted(objects_list)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***getlist_completable_progress* function** to get the full list of ompletables progress contained in 0 and 1." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def getlist_completable_progress():\n", - " completables=set()\n", - " completables_increase_decrease=set()\n", - " for player in players_info.keys():\n", - " cplayer = players_info[player][\"completables_progress\"]\n", - " if cplayer != {}:\n", - " for c in cplayer.keys():\n", - " allvalin=True\n", - " for i in range(len(cplayer[c])):\n", - " temp=cplayer[c][i][0]\n", - " if temp<0 or temp>1:\n", - " allvalin=False\n", - " if allvalin:\n", - " completables.add(c)\n", - " else:\n", - " completables_increase_decrease.add(c)\n", - " return sorted(list(completables)), sorted(list(completables_increase_decrease))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "***getSublistSelectedFromUnionListSelected* function** to get the elements selected from full list that are in sublist.\n", - "\n", - "Input:\n", - "* sublist : the sublist from where we want all elements present contained in unionListSelected\n", - "* unionListSelected : full list with selected elements" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "def getSublistSelectedFromUnionListSelected(sublist, unionListSelected):\n", - " subListSelected=[elem for elem in unionListSelected if elem in sublist]\n", - " return sorted(subListSelected)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/vis/xAPISG-PlayersProgress.ipynb b/vis/xAPISG-PlayersProgress.ipynb deleted file mode 100644 index ca9e835..0000000 --- a/vis/xAPISG-PlayersProgress.ipynb +++ /dev/null @@ -1,165 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%run vis/xAPISG-noDataToFillVisualization.ipynb # notebook to create the visualisation with a message NoDataToFill\n", - "%run vis/helpersFunctions/TimeLineChart.ipynb" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Selectors :\n", - " * absoluteToRelativePlayerProgress : checkboxe (T/F) value to display the line chart per relative time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "#constructing a checkboxe to view chart per relative time\n", - "absoluteToRelativePlayerProgress=widgets.Checkbox(value=False,\n", - " description='View in relative time')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***vis_players_progress* function**:\n", - "* output : the outputPlayerProgress in a list to be display in a VBox\n", - "* outputPlayerProgress : the line chart showing progress over time for each player" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "def vis_players_progress():\n", - " def display_player_progress(change):\n", - " with outputPlayerProgress:\n", - " clear_output(wait=False)\n", - " players_progress()\n", - " clear_output(wait=True)\n", - " absoluteToRelativePlayerProgress.observe(display_player_progress, 'value')\n", - " display_player_progress(None)\n", - " return [absoluteToRelativePlayerProgress, outputPlayerProgressUpdate, outputPlayerProgress]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***vis_players_progress* function** displays line chart showing progress over time for each player" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def players_progress():\n", - " #close and clear previous fig of progress players\n", - " for fig in figPlayersProgress:\n", - " plt.close(fig=fig)\n", - " figPlayersProgress.clear()\n", - " #get player selected list\n", - " playersSelected=checkboxesPlayersSelected.get_selected_options()\n", - " empty=True\n", - " #create new fig with custom name in the tab of player progress figs\n", - " name=filename+\"_players_progress\"\n", - " fig = plt.figure(num=name, constrained_layout=True)\n", - " fig.canvas.header_visible = False\n", - " fig.canvas.layout.min_height = '400px'\n", - " figPlayersProgress.append(fig)\n", - " #display a linechart for each player\n", - " for player in players_info.keys():\n", - " if player in playersSelected:\n", - " if players_info[player][\"game_progress_per_time\"] != []:\n", - " #list of time and progress value\n", - " progress_time = list(zip(*players_info[player][\"game_progress_per_time\"]))\n", - " y = [i*100 for i in progress_time[0]]\n", - " if absoluteToRelativePlayerProgress.value:\n", - " x = absoluteToRelativeTime(progress_time[1])\n", - " plt.xlabel('Relative Time')\n", - " else:\n", - " locator = mdates.AutoDateLocator()\n", - " plt.gca().xaxis.set_major_locator(locator)\n", - " plt.gca().xaxis.set_major_formatter(mdates.AutoDateFormatter(locator))\n", - " x =progress_time[1]\n", - " plt.xlabel('Time')\n", - " plt.plot(x,y, label=player) # one line chart per player showing progress over time\n", - " empty=False\n", - " if not empty:\n", - " #legend graph and give a date format\n", - " plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n", - " plt.yticks(np.arange(0,101,10))\n", - " else:\n", - " #display not data to fill vis\n", - " noDataToFillVis(100)\n", - " #orient xticks\n", - " plt.xticks(rotation=45, ha=\"right\")\n", - " #add title and axes labels\n", - " if absoluteToRelativePlayerProgress.value:\n", - " plt.xlabel('Relative Time')\n", - " else:\n", - " plt.xlabel('Time')\n", - " plt.title(\"Progress (in %) of players by time\")\n", - " plt.ylabel('Progress (in %)')\n", - " plt.show()\n", - " with outputPlayerProgressUpdate:\n", - " clear_output(wait=False)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/vis/xAPISG-VideosSeenSkipped.ipynb b/vis/xAPISG-VideosSeenSkipped.ipynb deleted file mode 100644 index 44854e3..0000000 --- a/vis/xAPISG-VideosSeenSkipped.ipynb +++ /dev/null @@ -1,283 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "is_executing": false, - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%run vis/xAPISG-noDataToFillVisualization.ipynb # notebook to create the visualisation with a message NoDataToFill\n", - "%run vis/helpersFunctions/barCharts.ipynb\n", - "%run vis/helpersFunctions/OrderGraphValuesAndSelectDataByXaxis.ipynb" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Selectors :\n", - " * xaxisVideo : dropdown value indicating the number of bar per chart (Alls, 5, 25, 50, 10 first, 10 last)\n", - " * orderVideo : dropdown value indicating how to order xlabel : alphabetically or by y value (hight to low or low to hight)\n", - " * percentageVideoSkipped : checkboxe (T/F) value to display the bar chart in percentage or not\n", - " * orderLabelValuesVideo : dropdown value indicating how to order labels : by the sum of the 2 superposed values or by video seen or skipped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "#constructing a dropdown to select how many data we want to see in visualisations for x axis\n", - "xaxisVideo=widgets.Dropdown(options=[('All data',0),('5 per visualisation',5),('25 per visualisation',25),('50 per visualisation',50),('10 first',-1),('10 last',-2)],\n", - " description='x axe',\n", - " disabled=False)\n", - "#constructing a dropdown to order for alphabetic keys or for values\n", - "orderVideo=widgets.Dropdown(options=[('alphabetic',0),('value from highest to lowest',1),('value from lowest to highest',-1)],\n", - " description='Order',\n", - " disabled=False)\n", - "#constructing a checkboxe to view chart per percentage video skipped\n", - "percentageVideoSkipped=widgets.Checkbox(value=False,\n", - " description='Show % per video skipped')\n", - "#constructing a dropdown value indicating how to order labels : by the sum of the 2 superposed values or by video seen or skipped\n", - "orderLabelValuesVideo=widgets.Dropdown(options=[(\"Order by all seen and skipped videos values\",0),\n", - " (\"Order by seens videos values\",\"seen\"),\n", - " (\"Order by skipped videos values\",\"skipped\")],\n", - " description='Change order label',\n", - " disabled=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***vis_videos_seen_skipped* function**:\n", - "* Output : the widgets selector and the outputVideo in a list to be display in a VBox\n", - "* outputVideo : display a bar chart showing for each video the total number of times it has been seen and skipped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def vis_videos_seen_skipped():\n", - " def display_video_vis(change):\n", - " if orderVideo.value==0 or percentageVideoSkipped.value:\n", - " orderLabelValuesVideo.layout=layout_hidden\n", - " else:\n", - " orderLabelValuesVideo.layout=layout_visible\n", - " with outputVideo:\n", - " clear_output(wait=False)\n", - " construct_graph_video()\n", - " clear_output(wait=True)\n", - " xaxisVideo.observe(display_video_vis, 'value')\n", - " orderVideo.observe(display_video_vis, 'value')\n", - " percentageVideoSkipped.observe(display_video_vis, 'value')\n", - " orderLabelValuesVideo.observe(display_video_vis, 'value')\n", - " display_video_vis(None)\n", - " return [percentageVideoSkipped, xaxisVideo, orderVideo, orderLabelValuesVideo, outputVideoUpdate, outputVideo]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "***construct_graph_video* function**:\n", - "\n", - "Output : Displays bar chart showing for each video the total number of times it has been seen and skipped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def construct_graph_video():\n", - " #get all players and videos selected\n", - " playersSelected=checkboxesPlayersSelected.get_selected_options()\n", - " videosSelected=checkboxesVideoSelected.get_selected_options()\n", - " if len(videosSelected)>0 and len(playersSelected)>0:\n", - " if xaxisVideo.value in [0,-1,-2]:\n", - " nbgraph=1\n", - " else:\n", - " div=len(videosSelected)/xaxisVideo.value\n", - " nbgraph=math.ceil(div)\n", - " accordionVideo=widgets.Accordion(children=[widgets.Output() for i in range(nbgraph)],\n", - " selected_index=None)\n", - " if nbgraph>1:\n", - " for i in range(nbgraph):\n", - " accordionVideo.set_title(i, str(i+1)+\"/\"+str(nbgraph))\n", - " display(accordionVideo)\n", - " def on_selected_index_video_change(change):\n", - " n=change['new']\n", - " if n!=None:\n", - " #close and clear previous fig of videos\n", - " for fig in figVideoSkipped:\n", - " plt.close(fig=fig)\n", - " figVideoSkipped.clear()\n", - " #get all players and videos selected\n", - " playersSelected=checkboxesPlayersSelected.get_selected_options()\n", - " videosSelected=checkboxesVideoSelected.get_selected_options()\n", - " #dict with seen and skipped video\n", - " videos_seen = {}\n", - " videos_skipped = {}\n", - " for player in playersSelected:\n", - " # videos seen\n", - " for video_seen in players_info[player][\"videos_seen\"]:\n", - " if video_seen in videosSelected:\n", - " if video_seen in videos_seen.keys():\n", - " videos_seen[video_seen] += 1\n", - " else:\n", - " videos_seen[video_seen] = 1\n", - " if video_seen not in videos_skipped.keys():\n", - " videos_skipped[video_seen] = 0\n", - " # videos skipped\n", - " for video_skipped in players_info[player][\"videos_skipped\"]:\n", - " if video_skipped in videosSelected:\n", - " if video_skipped in videos_skipped.keys():\n", - " videos_skipped[video_skipped] += 1\n", - " else:\n", - " videos_skipped[video_skipped] = 1\n", - " if video_skipped not in videos_seen.keys():\n", - " videos_seen[video_skipped] = 0\n", - " else:\n", - " videos_seen[video_skipped] -= 1\n", - " #reorganize all in a dictionary\n", - " videosseenskipped={}\n", - " for video in videosSelected:\n", - " if video in videos_seen.keys():\n", - " videosseenskipped[video]={\"seen\":videos_seen[video],\"skipped\":videos_skipped[video]}\n", - " else:\n", - " videosseenskipped[video]={\"seen\":0,\"skipped\":0}\n", - " #define order of labels values\n", - " if orderLabelValuesVideo.value==0:\n", - " lambdafunc=lambda item:item[1][\"seen\"]+item[1][\"skipped\"]\n", - " else:\n", - " lambdafunc=lambda item:item[1][orderLabelValuesVideo.value]\n", - " if percentageVideoSkipped.value:\n", - " lambdafunc=lambda item:item[1][\"skipped\"]/(item[1][\"seen\"]+item[1][\"skipped\"])\n", - " #order alls values\n", - " videosseenskipped = orderDict(videosseenskipped,orderVideo.value,lambdafunc)\n", - " #separe in sub dict in function of xaxis dropout selected\n", - " nbgraphshown, videosseenskipped,N = organizeDictPerxAxisSelected(videos_seen,xaxisVideo.value, videosseenskipped)\n", - " # Bar chart showing for each video\n", - " # the total number of times it has been seen and skipped\n", - " #get value for video seen and skipped\n", - " v_seen=[videosseenskipped[n][video][\"seen\"] for video in videosseenskipped[n].keys()]\n", - " v_skipped=[videosseenskipped[n][video][\"skipped\"] for video in videosseenskipped[n].keys()]\n", - " v_list=[video for video in videosseenskipped[n].keys()]\n", - " #create output for this graph\n", - " with accordionVideo.children[n]:\n", - " clear_output(wait=False)\n", - " #create new fig with custom name in the tab\n", - " name=filename+\"_videos_seen_skipped\"\n", - " if percentageVideoSkipped.value:\n", - " name+=\"_percentage_skipped\"\n", - " if nbgraphshown>1:\n", - " name+=\"_\"+str(n+1)+\"_\"+str(nbgraphshown)\n", - " fig=plt.figure(num=name, constrained_layout=True)\n", - " fig.canvas.header_visible = False\n", - " fig.canvas.layout.min_height = '400px'\n", - " figVideoSkipped.append(fig)\n", - " #display graph\n", - " if percentageVideoSkipped.value:\n", - " #display percentage bar\n", - " get_percentage_bar(v_skipped, v_seen, v_list)\n", - " plt.ylabel(\"Percentage of videos skipped\")\n", - " else:\n", - " #display superposed graph\n", - " if n 0: + global result_progress_column + result_progress_column=result_progress_columns[0] + progress_data=df.loc[df['verb.id'].isin(progress_verbs)].copy() + progress_data=progress_data.loc[progress_data["object.definition.type"]==seriousgame_object] + progress_data[result_progress_column] = progress_data.apply(update_progress, axis=1) + bar_game_data=progress_data.loc[progress_data['object.id'] == game].copy() + return bar_game_data + +def displayPlayerProgressInitFig(bar_game_data, game): + if isinstance(bar_game_data, pd.DataFrame) and len(bar_game_data) >0: + data = [] + for actor in bar_game_data['actor.name'].unique(): + bar_data=bar_game_data.loc[bar_game_data['actor.name'] == actor].copy() + first =bar_data.timestamp.min() + bar_data["timestampinit"] = pd.to_timedelta(pd.to_datetime(bar_data['timestamp']) - pd.to_datetime(first)) + pd.to_datetime('1970/01/01') + data.append(go.Scatter(x=bar_data['timestampinit'], y=bar_data[result_progress_column], name=actor, hovertext=f"{actor}", mode="lines+markers")) + fig = go.Figure(data=data) + else: + from vis import xAPISGnoDataToFillVisualization + fig=xAPISGnoDataToFillVisualization.noDataToFillVis(10) + fig.update_layout( + title_text='Progress (same origin) during game level ' + str(game), + ) + fig.update_xaxes(categoryorder="total descending") + return fig + +def displayPlayerProgressFig(bar_game_data, game): + if isinstance(bar_game_data, pd.DataFrame) and len(bar_game_data) >0: + data = [] + for actor in bar_game_data['actor.name'].unique(): + bar_data=bar_game_data.loc[bar_game_data['actor.name'] == actor].copy() + data.append(go.Scatter(x=bar_data['timestamp'], y=bar_data[result_progress_column], name=actor, hovertext=f"{actor}", mode="lines+markers")) + fig = go.Figure(data=data) + else: + from vis import xAPISGnoDataToFillVisualization + fig=xAPISGnoDataToFillVisualization.noDataToFillVis(10) + fig.update_layout( + title_text='Progress during game level ' + str(game), + ) + fig.update_xaxes(categoryorder="total descending") + return fig + +# Define a function to update the progress for specific verbs +def update_started_completed_progress(row): + if row['timestamp_started'] and row['timestamp_completed']: + return f"Started And Completed" + else: + return f"Only Started" + +def ProgressPlayerPie(df, game): + progress_data=df.loc[df['verb.id'].isin(progress_verbs)].copy() + progress_game=progress_data.loc[progress_data["object.definition.type"]==seriousgame_object] + progress_game=progress_game.loc[progress_game["object.id"] == game] + + # Filter for "started" and "completed" + progress_game_started = progress_game[progress_game['verb.id'] == initialized_verb_id] + progress_game_completed = progress_game[progress_game['verb.id'] == completed_verb_id] + + # Merge to find actors who started and completed the same object + progress_game_merged = pd.merge(progress_game_started, progress_game_completed, on=['actor.name'], suffixes=('_started', '_completed')) + + progress_game_merged["startedCompleted"] = progress_game_merged.apply(update_started_completed_progress, axis=1) + #pie_chart_data=progress_game_merged.groupby(["startedCompleted"])["actor.name"].apply(list).reset_index(name='Actors') + pie_chart_data=progress_game_merged.groupby(["startedCompleted"])["actor.name"].value_counts().reset_index(name='count') + #if len(pie_chart_data["Actors"]) > 0: + # pie_chart_data["Actors"]= list(set(pie_chart_data["AllActors"][0])) + # pie_chart_data["count"]= len(pie_chart_data["Actors"][0]) + #else: + # pie_chart_data["Actors"]=[] + # pie_chart_data["count"]=0 + return pie_chart_data + +def displayPlayerProgressPieFig(pie_chart_data, game): + if isinstance(pie_chart_data, pd.DataFrame) and len(pie_chart_data)>0: + fig = go.Figure(data=[go.Pie( + labels=pie_chart_data['startedCompleted'], + values=pie_chart_data['count'], + hoverinfo='label+percent+value+text', + text=pie_chart_data['startedCompleted'] + )]) + else: + from vis import xAPISGnoDataToFillVisualization + fig=xAPISGnoDataToFillVisualization.noDataToFillVis(10) + fig.update_layout( + title_text=f'Game Status of {game}' + ) + ## Show the plot + return fig \ No newline at end of file diff --git a/vis/xAPISGVideosSeenSkipped.py b/vis/xAPISGVideosSeenSkipped.py new file mode 100644 index 0000000..41dbd9c --- /dev/null +++ b/vis/xAPISGVideosSeenSkipped.py @@ -0,0 +1,28 @@ +import pandas as pd +import plotly.express as px + +def VideoSeenSkippedBarChart(df): + only_cutscenes = df[df['object.definition.type'].str.contains('cutscene', case=False)] + df_skipped = only_cutscenes[only_cutscenes['verb.id'].str.contains('skipped')] + df_accessed = only_cutscenes[only_cutscenes['verb.id'].str.contains('accessed')] + print("Skipped " + str(len(df_skipped))) + print("Access " + str(len(df_accessed))) + # Merge to find actors who start and skipped the same object + df_access_skipped = pd.merge(df_skipped, df_accessed, on=['object.id','actor.name'], how= 'outer', suffixes=('_skipped', '_accessed')) + df_access_skipped + + # Group by 'object' and 'actor' to get the count + df_grouped = df_access_skipped[['object.id', 'actor.name', 'verb.id_skipped', 'verb.id_accessed']].groupby(['object.id', 'actor.name']).count().reset_index() + df_grouped['count_seen_but_skipped']=df_grouped['verb.id_skipped'] + df_grouped['count_seen']=df_grouped['verb.id_accessed']-df_grouped['verb.id_skipped'] + if len(df_grouped) > 0: + # Create a stacked bar chart with Plotly + fig = px.bar(df_grouped, x='object.id', y=['count_seen', 'count_seen_but_skipped'], + color_discrete_sequence=['#1f77b4', '#ff7f0e'], + hover_name='actor.name', + labels={'count_seen_but_skipped': 'Access Count', 'count_seen': 'Seen Count', 'actor.name':'Actor'}) + ### Show the plot + return fig + else: + from vis import xAPISGnoDataToFillVisualization + return xAPISGnoDataToFillVisualization.noDataToFillVis(10) \ No newline at end of file diff --git a/vis/xAPISGnoDataToFillVisualization.py b/vis/xAPISGnoDataToFillVisualization.py new file mode 100644 index 0000000..f5e8c94 --- /dev/null +++ b/vis/xAPISGnoDataToFillVisualization.py @@ -0,0 +1,29 @@ +# ***noDataToFillVis* function** display a text saying "There is no data to fill the visualization." when no data are given to a certain visualisation +# Input : +# * max : the max y value +# * showYLabel : False if the y value labels are not display, otherwise the y value labels are display + +import plotly.graph_objs as go +import numpy as np +def noDataToFillVis(max_val, showYLabel=False): + fig = go.Figure() + + fig.add_annotation( + x=0.5, + y=max_val / 2, + text="There is no data to fill the visualization.", + showarrow=False, + font=dict(size=10), + xref='paper', + yref='y' + ) + + fig.update_xaxes(showticklabels=False) + + if not showYLabel: + fig.update_yaxes(showticklabels=False) + + fig.update_yaxes(range=[0, max_val]) + fig.update_yaxes(tickvals=list(np.arange(0, max_val, max_val / 10))) + + return fig \ No newline at end of file diff --git a/widgets/multipleSelectorCheckboxesWidget.ipynb b/widgets/multipleSelectorCheckboxesWidget.ipynb deleted file mode 100644 index 1803da4..0000000 --- a/widgets/multipleSelectorCheckboxesWidget.ipynb +++ /dev/null @@ -1,184 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "Adapted from: https://gist.github.com/MattJBritton/9dc26109acb4dfe17820cf72d82f1e6f\n", - "\n", - "Usage\n", - "\n", - "import ipywidgets as widgets\n", - "\n", - "class MultiCheckboxWidgetI(MultiCheckboxWidget):\n", - "\n", - " def update(self):\n", - "\n", - " # implements this method\n", - "\n", - "ui = MultiCheckboxWidgetI(['hello','world'])\n", - "\n", - "display(wid.HBox([ui.multi_select]))\n", - "\n", - "To get the selected options:\n", - "selected_options = ui.get_selected_options()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "class MultiCheckboxWidget(object):\n", - " def __init__(self, name, options_list):\n", - " self.name = widgets.HTML(name)\n", - " \"\"\" Widget with a search field and lots of checkboxes \"\"\"\n", - " self.options_dict = {\n", - " x: widgets.Checkbox(\n", - " description=x,\n", - " value=True,\n", - " style={\"description_width\":\"0px\"}\n", - " ) for x in options_list\n", - " }\n", - "\n", - " self.search_widget = widgets.Text()\n", - " self.output_widget = widgets.Output()\n", - " self.options = [x for x in self.options_dict.values()]\n", - " self.options_layout = widgets.Layout(\n", - " overflow='auto',\n", - " border='1px solid black',\n", - " width='300px',\n", - " height='300px',\n", - " flex_flow='column',\n", - " display='flex'\n", - " )\n", - "\n", - " self.select_all_button = widgets.Button(description='Select All')\n", - " self.select_any_button = widgets.Button(description='Unselect All')\n", - " self.select_widget=widgets.HBox([self.select_any_button, self.select_all_button])\n", - " self.options_widget = widgets.VBox(self.options, layout=self.options_layout)\n", - " self.automatic_update_checkboxe = widgets.Checkbox(\n", - " description=\"Automatic update\",\n", - " value=True,\n", - " style={\"description_width\":\"0px\"}\n", - " )\n", - " self.manual_update_button=widgets.Button(description='Update')\n", - " def manual_update(change):\n", - " self.update()\n", - " self.manual_update_button.on_click(manual_update)\n", - "\n", - " self.automatic_HBox=widgets.HBox([self.automatic_update_checkboxe], layout=widgets.Layout(width='300px'))\n", - "\n", - " self.multi_select = widgets.VBox([self.name, self.select_widget,\n", - " self.search_widget, self.options_widget,\n", - " self.automatic_HBox])\n", - "\n", - " self.automatic_update = True\n", - "\n", - " def on_select_all_button(change):\n", - " self.automatic_update=False\n", - " for x in self.options:\n", - " x.value=True\n", - " new_options=sorted(\n", - " [x for x in self.options],\n", - " key = lambda x: x.description\n", - " )\n", - " self.options_widget.children = new_options\n", - " self.automatic_update=self.automatic_update_checkboxe.value\n", - " if self.automatic_update:\n", - " self.update()\n", - " self.select_all_button.on_click(on_select_all_button)\n", - "\n", - " def on_select_automatic_update_checkboxe(change):\n", - " if change['new']:\n", - " self.automatic_update=True\n", - " self.automatic_HBox.children=[self.automatic_update_checkboxe]\n", - " self.update()\n", - " else:\n", - " self.automatic_update=False\n", - " self.automatic_HBox.children=[self.automatic_update_checkboxe, self.manual_update_button]\n", - " self.automatic_update_checkboxe.observe(on_select_automatic_update_checkboxe, names=\"value\")\n", - "\n", - " def on_select_any_button(change):\n", - " self.automatic_update=False\n", - " for x in self.options:\n", - " x.value=False\n", - " new_options=sorted(\n", - " [x for x in self.options],\n", - " key = lambda x: x.description\n", - " )\n", - " self.options_widget.children=new_options\n", - " self.automatic_update=self.automatic_update_checkboxe.value\n", - " if self.automatic_update:\n", - " self.update()\n", - " self.select_any_button.on_click(on_select_any_button)\n", - "\n", - " @self.output_widget.capture()\n", - " def on_checkbox_change(change):\n", - " selected_recipe = change[\"owner\"].description\n", - " self.options_widget.children = sorted(\n", - " [x for x in self.options_widget.children],\n", - " key = lambda x: x.value, reverse = True\n", - " )\n", - " if self.automatic_update:\n", - " self.update()\n", - "\n", - " for checkbox in self.options:\n", - " checkbox.observe(on_checkbox_change, names=\"value\")\n", - "\n", - " # Wire the search field to the checkboxes\n", - " @self.output_widget.capture()\n", - " def on_text_change(change):\n", - " self.search_input = change['new']\n", - " if self.search_input == '':\n", - " # Reset search field\n", - " new_options = sorted(self.options, key = lambda x: x.value, reverse = True)\n", - " else:\n", - " # Filter by search field using difflib.\n", - " close_matches = [x for x in list(self.options_dict.keys()) if str.lower(self.search_input.strip('')) in str.lower(x)]\n", - " new_options = sorted(\n", - " [x for x in self.options if x.description in close_matches],\n", - " key = lambda x: x.value, reverse = True\n", - " )\n", - " self.options_widget.children = new_options\n", - "\n", - " self.search_widget.observe(on_text_change, names='value')\n", - "\n", - " def get_selected_options(self):\n", - " return [widget.description for widget in self.options_dict.values() if widget.value]" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/widgets/selectFileWidget.ipynb b/widgets/selectFileWidget.ipynb deleted file mode 100644 index fb6f017..0000000 --- a/widgets/selectFileWidget.ipynb +++ /dev/null @@ -1,122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "source": [ - "Adapted from : https://gist.github.com/DrDub/6efba6e522302e43d055\n", - "\n", - "# example usage:\n", - " f = FileBrowser(accept=\".{filetype}\")\n", - "\n", - " f.widget()\n", - "\n", - " \n", - "\n", - "# in a separate cell:\n", - " f.path # returns the selected path" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } - }, - { - "cell_type": "code", - "execution_count": 1, - "outputs": [], - "source": [ - "import os\n", - "\n", - "class FileBrowser(object):\n", - " def __init__(self, accept):\n", - " self.path = os.getcwd()\n", - " self._update_files()\n", - " self.accept=accept\n", - " self.layoutButtons = widgets.Layout(width='auto')\n", - " self.buttonRun = widgets.Button(description='Run analyse', button_style='success', visible=False,\n", - " layout=self.layoutButtons)\n", - " self.output=widgets.Output(layout={'border': '1px solid black'})\n", - "\n", - " def _update_files(self):\n", - " self.files = list()\n", - " self.dirs = list()\n", - " if(os.path.isdir(self.path)):\n", - " for f in os.listdir(self.path):\n", - " ff = os.path.join(self.path, f)\n", - " if os.path.isdir(ff):\n", - " self.dirs.append(f)\n", - " else:\n", - " self.files.append(f)\n", - "\n", - " def widget(self):\n", - " box = widgets.VBox()\n", - " self._update(box)\n", - " return box\n", - "\n", - " def _update(self, box):\n", - " def on_click(b):\n", - " if b.description == '..':\n", - " self.path = os.path.split(self.path)[0]\n", - " with self.output:\n", - " clear_output()\n", - " else:\n", - " self.path = os.path.join(self.path, b.description)\n", - " self._update_files()\n", - " self._update(box)\n", - "\n", - " buttons = []\n", - " button = widgets.Button(description='..', background_color='#d0d0ff', layout=self.layoutButtons)\n", - " button.on_click(on_click)\n", - " buttons.append(button)\n", - " if(os.path.isdir(self.path)):\n", - " for f in self.dirs:\n", - " button = widgets.Button(description=f, background_color='#d0d0ff', layout=self.layoutButtons)\n", - " button.on_click(on_click)\n", - " buttons.append(button)\n", - " for f in self.files:\n", - " fileExtension = os.path.splitext(f)[1]\n", - " if fileExtension == self.accept:\n", - " button = widgets.Button(description=f, button_style='success', layout=self.layoutButtons)\n", - " button.on_click(on_click)\n", - " buttons.append(button)\n", - " if(os.path.isfile(self.path)):\n", - " self.buttonRun.visible=True\n", - " buttons.append(self.buttonRun)\n", - " self.output.clear_output(wait=False)\n", - " with self.output:\n", - " print(\"Click to run analyse to the file\")\n", - " buttons.append(self.output)\n", - " box.children = tuple([widgets.HTML(\"

%s

\" % (self.path,))] + buttons)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} \ No newline at end of file diff --git a/widgets/simvaWidget.ipynb b/widgets/simvaWidget.ipynb deleted file mode 100644 index fb0a4be..0000000 --- a/widgets/simvaWidget.ipynb +++ /dev/null @@ -1,177 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import requests\n", - "import xml.etree.ElementTree as ET\n", - "\n", - "# boto3 not installed by default in anaconda (conda install boto3)\n", - "import boto3\n", - "import boto3.session\n", - "import pandas as pd\n", - "import io\n", - "from jwt import JWT\n", - "from botocore.client import Config\n", - "\n", - "class SimvaBrowser(object):\n", - " def __init__(self, auth, storage_url, accept='.json', ca_file=None, bucket_name='traces', delimiter='/'):\n", - " self.auth = auth \n", - " self.storage_url = storage_url;\n", - " self.accept='.json';\n", - " self.ca_file = ca_file;\n", - " self.bucket_name = bucket_name;\n", - " self.delimiter = delimiter;\n", - " jwt_parser = JWT()\n", - " self.access_token = jwt_parser.decode(self.auth.access_token, do_verify=False)\n", - "\n", - " self._storage_login();\n", - " \n", - " self.base_path = 'users' + self.delimiter + self.access_token['preferred_username'] + self.delimiter;\n", - " self.current_path = self.base_path;\n", - "\n", - " self._update_files()\n", - " \n", - " self.layoutButtons = widgets.Layout(width='auto')\n", - " self.buttonRun = widgets.Button(description='Run analyse', button_style='success', visible=False,\n", - " layout=self.layoutButtons)\n", - " self.output=widgets.Output(layout={'border': '1px solid black'})\n", - "\n", - " def _storage_login(self):\n", - " data = {\n", - " 'Action':'AssumeRoleWithWebIdentity',\n", - " 'Version': '2011-06-15',\n", - " 'DurationSeconds' : 3600,\n", - " 'WebIdentityToken': self.auth.access_token\n", - " };\n", - " response = requests.post(self.storage_url, data=data, verify=self.ca_file)\n", - " if response.status_code != 200:\n", - " print('Problems getting temporary credentials')\n", - " print(response.text)\n", - " else:\n", - " ns = {'sts' : 'https://sts.amazonaws.com/doc/2011-06-15/'}\n", - " root = ET.fromstring(response.text)\n", - " credentials = root.find('sts:AssumeRoleWithWebIdentityResult', ns).find('sts:Credentials', ns)\n", - " self.access_key_id = credentials.find('sts:AccessKeyId', ns).text\n", - " self.secret_access_key = credentials.find('sts:SecretAccessKey', ns).text\n", - " self.session_token = credentials.find('sts:SessionToken', ns).text\n", - " \n", - " def _s3_client(self):\n", - " aws_session=boto3.session.Session(aws_access_key_id=self.access_key_id,\n", - " aws_secret_access_key=self.secret_access_key,\n", - " aws_session_token=self.session_token,\n", - " region_name='us-east-1')\n", - " return aws_session.client(service_name='s3'\n", - " , endpoint_url=self.storage_url\n", - " , verify=self.ca_file)\n", - "\n", - " def _list_files(self, path):\n", - " s3_client = self._s3_client();\n", - " folder = s3_client.list_objects(Bucket=self.bucket_name\n", - " , Prefix=path\n", - " , Delimiter=self.delimiter)\n", - " files = list()\n", - " contents = folder.get('Contents')\n", - " if contents:\n", - " for o in contents:\n", - " files.append(o.get('Key')[len(self.current_path):])\n", - " return files\n", - "\n", - " def _list_folders(self, path):\n", - " s3_client = self._s3_client();\n", - " folder = s3_client.list_objects(Bucket=self.bucket_name\n", - " , Prefix=path\n", - " , Delimiter=self.delimiter)\n", - " folders = list()\n", - " contents = folder.get('CommonPrefixes')\n", - " if contents:\n", - " for o in contents:\n", - " folders.append(o.get('Prefix')[len(self.current_path):])\n", - " return folders\n", - "\n", - " def get_file_content(self, path):\n", - " s3_client = self._s3_client();\n", - " file = s3_client.get_object(Bucket=self.bucket_name, Key=path)\n", - " return file['Body'].read()\n", - "\n", - " def _isdir(self, path):\n", - " return path[len(path)-1] == self.delimiter;\n", - "\n", - " \n", - " def _update_files(self):\n", - " self.files = list()\n", - " self.dirs = list()\n", - " if(self._isdir(self.current_path)):\n", - " self.dirs = self._list_folders(self.current_path)\n", - " self.files = self._list_files(self.current_path)\n", - "\n", - " def widget(self):\n", - " box = widgets.VBox()\n", - " self._update(box)\n", - " return box\n", - "\n", - " def _update(self, box):\n", - " def on_click(b):\n", - " if b.description == '..':\n", - " if len(self.current_path) > len(self.base_path):\n", - " self.current_path = self.current_path.rpartition(self.delimiter)[0].rpartition(self.delimiter)[0] + self.delimiter\n", - " else:\n", - " self.current_path = self.base_path\n", - " with self.output:\n", - " clear_output()\n", - " else:\n", - " self.current_path = self.current_path + b.description\n", - " self._update_files()\n", - " self._update(box)\n", - "\n", - " buttons = []\n", - " button = widgets.Button(description='..', background_color='#d0d0ff', layout=self.layoutButtons)\n", - " button.on_click(on_click)\n", - " buttons.append(button)\n", - " if(self._isdir(self.current_path)):\n", - " for f in self.dirs:\n", - " button = widgets.Button(description=f, background_color='#d0d0ff', layout=self.layoutButtons)\n", - " button.on_click(on_click)\n", - " buttons.append(button)\n", - " for f in self.files:\n", - " fileExtension = os.path.splitext(f)[1]\n", - " if fileExtension == self.accept:\n", - " button = widgets.Button(description=f, button_style='success', layout=self.layoutButtons)\n", - " button.on_click(on_click)\n", - " buttons.append(button)\n", - " else:\n", - " self.buttonRun.visible=True\n", - " buttons.append(self.buttonRun)\n", - " self.output.clear_output(wait=False)\n", - " with self.output:\n", - " print(\"Click to run analyse to the file\")\n", - " buttons.append(self.output)\n", - " box.children = tuple([widgets.HTML(\"

%s

\" % (self.current_path,))] + buttons)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/xapi-sg-sample-data-array.json b/xapi-sg-sample-data-array.json index 6b01763..a971a45 100644 --- a/xapi-sg-sample-data-array.json +++ b/xapi-sg-sample-data-array.json @@ -7,9 +7,9 @@ "id": "http://adlnet.gov/expapi/verbs/initialized" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "timestamp": "2016-05-24T15:03:47Z" @@ -24,7 +24,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "timestamp": "2016-05-24T15:03:47Z" @@ -39,13 +39,12 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.5 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -58,15 +57,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.15 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.15 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -79,15 +77,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.5 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5 } }, "timestamp": "2016-05-24T15:15:49Z" @@ -100,15 +97,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.7 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.7 } }, "timestamp": "2016-05-24T15:25:49Z" @@ -121,15 +117,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.9 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.9 } }, "timestamp": "2016-05-24T15:35:49Z" @@ -144,14 +139,14 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 50 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 50 } }, "timestamp": "2016-05-24T15:40:13Z" @@ -164,16 +159,16 @@ "id": "http://adlnet.gov/expapi/verbs/completed" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Levels/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 50 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 50 } }, "timestamp": "2016-05-24T15:45:13Z" @@ -188,7 +183,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Screens/SoundMenu", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Screen" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Screen" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -201,9 +196,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -216,9 +211,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/VideoTutorial", + "id": "http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -231,9 +226,9 @@ "id": "http://id.tincanapi.com/verb/skipped" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T09:17:37Z" @@ -248,7 +243,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -267,7 +262,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -286,7 +281,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -305,7 +300,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -324,7 +319,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -343,7 +338,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -360,9 +355,9 @@ "id": "http://adlnet.gov/expapi/verbs/initialized" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "timestamp": "2016-05-24T15:05:47Z" @@ -377,7 +372,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "timestamp": "2016-05-24T15:05:47Z" @@ -392,13 +387,12 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.1 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.1 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -411,15 +405,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.1 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.1 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -432,15 +425,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.35 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.35 } }, "timestamp": "2016-05-24T15:15:49Z" @@ -453,15 +445,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.55 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.55 } }, "timestamp": "2016-05-24T15:25:49Z" @@ -474,15 +465,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.75 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.75 } }, "timestamp": "2016-05-24T15:35:49Z" @@ -497,14 +487,14 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "False", - "score": 17 + "http://example.com/extensions/success": "False", + "http://example.com/extensions/score": 17 } }, "timestamp": "2016-05-24T15:48:13Z" @@ -517,16 +507,16 @@ "id": "http://adlnet.gov/expapi/verbs/completed" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Levels/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "False", - "score": 33 + "http://example.com/extensions/success": "False", + "http://example.com/extensions/score": 33 } }, "timestamp": "2016-05-24T15:55:13Z" @@ -541,7 +531,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Screens/SoundMenu", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Screen" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Screen" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -554,9 +544,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -569,9 +559,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/VideoTutorial", + "id": "http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -584,9 +574,9 @@ "id": "http://id.tincanapi.com/verb/skipped" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T09:17:37Z" @@ -601,7 +591,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -620,7 +610,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -639,7 +629,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -658,7 +648,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -677,7 +667,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -696,7 +686,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -713,9 +703,9 @@ "id": "http://adlnet.gov/expapi/verbs/initialized" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "timestamp": "2016-05-24T15:03:47Z" @@ -730,7 +720,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "timestamp": "2016-05-24T15:03:47Z" @@ -745,13 +735,12 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.5 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -764,15 +753,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.45 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.45 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -785,15 +773,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.8 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.8 } }, "timestamp": "2016-05-24T15:15:49Z" @@ -806,15 +793,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.99 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.99 } }, "timestamp": "2016-05-24T15:35:49Z" @@ -829,14 +815,14 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 95 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 95 } }, "timestamp": "2016-05-24T15:37:13Z" @@ -849,16 +835,16 @@ "id": "http://adlnet.gov/expapi/verbs/completed" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Levels/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 95 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 95 } }, "timestamp": "2016-05-24T15:37:13Z" @@ -873,7 +859,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Screens/SoundMenu", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Screen" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Screen" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -886,9 +872,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -901,9 +887,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/VideoTutorial", + "id": "http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -916,9 +902,9 @@ "id": "http://id.tincanapi.com/verb/accessed" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Cutscenes/VideoDemo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/VideoDemo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T09:17:37Z" @@ -933,7 +919,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -952,7 +938,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -971,7 +957,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -988,9 +974,9 @@ "id": "http://adlnet.gov/expapi/verbs/initialized" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "timestamp": "2016-05-24T15:25:47Z" @@ -1005,7 +991,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "timestamp": "2016-05-24T15:05:47Z" @@ -1020,13 +1006,12 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.5 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5 } }, "timestamp": "2016-05-24T15:05:49Z" @@ -1039,15 +1024,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.25 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.25 } }, "timestamp": "2016-05-24T15:36:49Z" @@ -1060,15 +1044,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.35 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.35 } }, "timestamp": "2016-05-24T15:38:49Z" @@ -1081,15 +1064,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.65 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.65 } }, "timestamp": "2016-05-24T15:45:49Z" @@ -1102,15 +1084,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.75 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.75 } }, "timestamp": "2016-05-24T15:47:49Z" @@ -1125,14 +1106,14 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "False", - "score": 72 + "http://example.com/extensions/success": "False", + "http://example.com/extensions/score": 72 } }, "timestamp": "2016-05-24T15:48:13Z" @@ -1145,16 +1126,16 @@ "id": "http://adlnet.gov/expapi/verbs/completed" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Levels/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 72 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 72 } }, "timestamp": "2016-05-24T15:59:13Z" @@ -1169,7 +1150,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Screens/SoundMenu", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Screen" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Screen" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -1182,9 +1163,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -1197,9 +1178,9 @@ "id": "http://id.tincanapi.com/verb/skipped" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T09:17:37Z" @@ -1214,7 +1195,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -1233,7 +1214,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -1252,7 +1233,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -1269,9 +1250,9 @@ "id": "http://adlnet.gov/expapi/verbs/initialized" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "timestamp": "2016-05-24T15:13:47Z" @@ -1286,7 +1267,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "timestamp": "2016-05-24T15:15:47Z" @@ -1301,7 +1282,7 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "timestamp": "2016-05-24T15:25:47Z" @@ -1316,16 +1297,15 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.5 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5 } }, - "timestamp": "2016-05-24T15:05:49Z" + "timestamp": "2016-05-24T15:15:49Z" }, { "actor": { @@ -1335,15 +1315,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.3 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.3 } }, "timestamp": "2016-05-24T15:17:49Z" @@ -1356,15 +1335,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.5 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5 } }, "timestamp": "2016-05-24T15:25:49Z" @@ -1377,15 +1355,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.75 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.75 } }, "timestamp": "2016-05-24T15:33:49Z" @@ -1398,15 +1375,14 @@ "id": "http://adlnet.gov/expapi/verbs/progressed" }, "object": { - "id": "http://example.com/games/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { - "https://w3id.org/xapi/seriousgames/extensions/progress": 0.5, - "progress": 0.95 + "https://w3id.org/xapi/seriousgames/extensions/progress": 0.95 } }, "timestamp": "2016-05-24T15:40:49Z" @@ -1421,14 +1397,14 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-1", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "False", - "score": 22 + "http://example.com/extensions/success": "False", + "http://example.com/extensions/score": 22 } }, "timestamp": "2016-05-24T15:55:13Z" @@ -1443,14 +1419,14 @@ "object": { "id": "http://example.com/games/SuperMarioBros/Levels/World1-2", "definition": { - "type": "http://curatr3.com/define/type/level" + "type": "https://w3id.org/xapi/seriousgames/activity-types/level" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 60 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 60 } }, "timestamp": "2016-05-24T15:55:13Z" @@ -1463,16 +1439,16 @@ "id": "http://adlnet.gov/expapi/verbs/completed" }, "object": { - "id": "http://example.com/games/SuperMarioBros/Levels/MyFirstGame", + "id": "http://example.com/games/SuperMarioBros", "definition": { - "type": "http://curatr3.com/define/type/serious-game" + "type": "https://w3id.org/xapi/seriousgames/activity-types/serious-game" } }, "result": { "extensions": { "https://rage.e-ucm.es/xapi/ext/value": "Game Over", - "success": "True", - "score": 43 + "http://example.com/extensions/success": "True", + "http://example.com/extensions/score": 43 } }, "timestamp": "2016-05-24T15:45:13Z" @@ -1485,9 +1461,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/IntroVideo", + "id": "http://example.com/games/SuperMarioBros/cutscenes/IntroVideo", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -1500,9 +1476,9 @@ "id": "http://activitystrea.ms/schema/1.0/accessed" }, "object": { - "id": "http://example.com/games/cutscenes/VideoTutorial", + "id": "http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Cutscene" } }, "timestamp": "2016-05-24T07:47:47Z" @@ -1517,7 +1493,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfSpain", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -1536,7 +1512,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { @@ -1555,7 +1531,7 @@ "object": { "id": "http://example.com/games/Capitals/CapitalOfFlorida", "definition": { - "type": "https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative" + "type": "https://w3id.org/xapi/seriousgames/activity-types/Alternative" } }, "result": { diff --git a/xapi-sg-sample-data.json b/xapi-sg-sample-data.json index 05a6f59..1c2c6f8 100644 --- a/xapi-sg-sample-data.json +++ b/xapi-sg-sample-data.json @@ -1,84 +1,84 @@ -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"timestamp":"2016-05-24T15:03:47Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"timestamp":"2016-05-24T15:03:47Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.15}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.5}},"timestamp":"2016-05-24T15:15:49Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.7}},"timestamp":"2016-05-24T15:25:49Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.9}},"timestamp":"2016-05-24T15:35:49Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":50}},"timestamp":"2016-05-24T15:40:13Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":50}},"timestamp":"2016-05-24T15:45:13Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/VideoTutorial","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://id.tincanapi.com/verb/skipped"},"object":{"id":"http://example.com/games/SuperMarioBros/Cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Lisbon","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Madrid","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Orlando","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Tampa","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Tallahassee","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"timestamp":"2016-05-24T15:05:47Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"timestamp":"2016-05-24T15:05:47Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.1}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.1}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.35}},"timestamp":"2016-05-24T15:15:49Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.55}},"timestamp":"2016-05-24T15:25:49Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.75}},"timestamp":"2016-05-24T15:35:49Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"False","score":17}},"timestamp":"2016-05-24T15:48:13Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"False","score":33}},"timestamp":"2016-05-24T15:55:13Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/VideoTutorial","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://id.tincanapi.com/verb/skipped"},"object":{"id":"http://example.com/games/SuperMarioBros/Cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Lisbon","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Mallorca","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Orlando","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Tampa","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Jacksonville","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"timestamp":"2016-05-24T15:03:47Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"timestamp":"2016-05-24T15:03:47Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.45}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.8}},"timestamp":"2016-05-24T15:15:49Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.99}},"timestamp":"2016-05-24T15:35:49Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":95}},"timestamp":"2016-05-24T15:37:13Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":95}},"timestamp":"2016-05-24T15:37:13Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/VideoTutorial","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://id.tincanapi.com/verb/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Cutscenes/VideoDemo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Madrid","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Tallahassee","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"timestamp":"2016-05-24T15:25:47Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"timestamp":"2016-05-24T15:05:47Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.25}},"timestamp":"2016-05-24T15:36:49Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.35}},"timestamp":"2016-05-24T15:38:49Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.65}},"timestamp":"2016-05-24T15:45:49Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.75}},"timestamp":"2016-05-24T15:47:49Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"False","score":72}},"timestamp":"2016-05-24T15:48:13Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":72}},"timestamp":"2016-05-24T15:59:13Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://id.tincanapi.com/verb/skipped"},"object":{"id":"http://example.com/games/SuperMarioBros/Cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Lisbon","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Jacksonville","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"timestamp":"2016-05-24T15:13:47Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"timestamp":"2016-05-24T15:15:47Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"timestamp":"2016-05-24T15:25:47Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.3}},"timestamp":"2016-05-24T15:17:49Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.5}},"timestamp":"2016-05-24T15:25:49Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.75}},"timestamp":"2016-05-24T15:33:49Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5,"progress":0.95}},"timestamp":"2016-05-24T15:40:49Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"False","score":22}},"timestamp":"2016-05-24T15:55:13Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"http://curatr3.com/define/type/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":60}},"timestamp":"2016-05-24T15:55:13Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/MyFirstGame","definition":{"type":"http://curatr3.com/define/type/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","success":"True","score":43}},"timestamp":"2016-05-24T15:45:13Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/IntroVideo","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/cutscenes/VideoTutorial","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Madrid","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} -{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://rage.e-ucm.es/xapi/seriousgames/activities/Alternative"}},"result":{"response":"Tallahassee","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"timestamp":"2016-05-24T15:03:47Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"timestamp":"2016-05-24T15:03:47Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.15}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5}},"timestamp":"2016-05-24T15:15:49Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.7}},"timestamp":"2016-05-24T15:25:49Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.9}},"timestamp":"2016-05-24T15:35:49Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":50}},"timestamp":"2016-05-24T15:40:13Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":50}},"timestamp":"2016-05-24T15:45:13Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://id.tincanapi.com/verb/skipped"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Lisbon","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Madrid","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Orlando","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Tampa","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"John Smith"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Tallahassee","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"timestamp":"2016-05-24T15:05:47Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"timestamp":"2016-05-24T15:05:47Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.1}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.1}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.35}},"timestamp":"2016-05-24T15:15:49Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.55}},"timestamp":"2016-05-24T15:25:49Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.75}},"timestamp":"2016-05-24T15:35:49Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"False","http://example.com/extensions/score":17}},"timestamp":"2016-05-24T15:48:13Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"False","http://example.com/extensions/score":33}},"timestamp":"2016-05-24T15:55:13Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://id.tincanapi.com/verb/skipped"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Lisbon","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Mallorca","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Orlando","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Tampa","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Sarah Connor"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Jacksonville","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"timestamp":"2016-05-24T15:03:47Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"timestamp":"2016-05-24T15:03:47Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.45}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.8}},"timestamp":"2016-05-24T15:15:49Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.99}},"timestamp":"2016-05-24T15:35:49Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":95}},"timestamp":"2016-05-24T15:37:13Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":95}},"timestamp":"2016-05-24T15:37:13Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://id.tincanapi.com/verb/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/VideoDemo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Madrid","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"James Dean"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Tallahassee","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"timestamp":"2016-05-24T15:25:47Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"timestamp":"2016-05-24T15:05:47Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5}},"timestamp":"2016-05-24T15:05:49Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.25}},"timestamp":"2016-05-24T15:36:49Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.35}},"timestamp":"2016-05-24T15:38:49Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.65}},"timestamp":"2016-05-24T15:45:49Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.75}},"timestamp":"2016-05-24T15:47:49Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"False","http://example.com/extensions/score":72}},"timestamp":"2016-05-24T15:48:13Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":72}},"timestamp":"2016-05-24T15:59:13Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/Screens/SoundMenu","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Screen"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://id.tincanapi.com/verb/skipped"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T09:17:37Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Lisbon","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Mary Kate"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Jacksonville","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"timestamp":"2016-05-24T15:13:47Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"timestamp":"2016-05-24T15:15:47Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/initialized"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"timestamp":"2016-05-24T15:25:47Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5}},"timestamp":"2016-05-24T15:15:49Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.3}},"timestamp":"2016-05-24T15:17:49Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.5}},"timestamp":"2016-05-24T15:25:49Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.75}},"timestamp":"2016-05-24T15:33:49Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/progressed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://w3id.org/xapi/seriousgames/extensions/progress":0.95}},"timestamp":"2016-05-24T15:40:49Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-1","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"False","http://example.com/extensions/score":22}},"timestamp":"2016-05-24T15:55:13Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros/Levels/World1-2","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/level"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":60}},"timestamp":"2016-05-24T15:55:13Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/completed"},"object":{"id":"http://example.com/games/SuperMarioBros","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/serious-game"}},"result":{"extensions":{"https://rage.e-ucm.es/xapi/ext/value":"Game Over","http://example.com/extensions/success":"True","http://example.com/extensions/score":43}},"timestamp":"2016-05-24T15:45:13Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/IntroVideo","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://activitystrea.ms/schema/1.0/accessed"},"object":{"id":"http://example.com/games/SuperMarioBros/cutscenes/VideoTutorial","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Cutscene"}},"timestamp":"2016-05-24T07:47:47Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfSpain","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Madrid","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Miami","success":"False"},"timestamp":"2016-05-24T13:05:12Z"} +{"actor":{"name":"Juan Gomez"},"verb":{"id":"http://adlnet.gov/expapi/verbs/selected"},"object":{"id":"http://example.com/games/Capitals/CapitalOfFlorida","definition":{"type":"https://w3id.org/xapi/seriousgames/activity-types/Alternative"}},"result":{"response":"Tallahassee","success":"True"},"timestamp":"2016-05-24T13:05:12Z"} From 82868140281220ff1559e8120d4e9ee5209de4cf Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER Date: Fri, 16 May 2025 10:21:29 +0200 Subject: [PATCH 3/4] fix dash app to serve locally --- TMonApp.py | 2 +- TMonSimvaApp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TMonApp.py b/TMonApp.py index c77f142..49453b5 100644 --- a/TMonApp.py +++ b/TMonApp.py @@ -2,7 +2,7 @@ from TMonWidgets import UploadWidget, TMonWidget # Initialize the app -TMonApp = Dash(__name__) +TMonApp = Dash(__name__, serve_locally=True) # App layout TMonApp.layout = html.Div([ TMonWidget.TMonHeader, diff --git a/TMonSimvaApp.py b/TMonSimvaApp.py index 28925d3..62d0b7b 100644 --- a/TMonSimvaApp.py +++ b/TMonSimvaApp.py @@ -2,7 +2,7 @@ from TMonWidgets import SimvaBrowserWidget, TMonWidget # Initialize Dash app with Flask server -app = Dash(__name__, server=SimvaBrowserWidget.flask.flaskServer) +app = Dash(__name__, server=SimvaBrowserWidget.flask.flaskServer, serve_locally=True) # Layout of the dashboard app.layout=html.Div( From d1d882102bf2dd04c9eda9218e00fd2182312d43 Mon Sep 17 00:00:00 2001 From: Julio SANTILARIO BERTHILIER Date: Fri, 16 May 2025 10:22:00 +0200 Subject: [PATCH 4/4] remove ssl for developement command --- SimvaBrowser/KeycloakClient.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/SimvaBrowser/KeycloakClient.py b/SimvaBrowser/KeycloakClient.py index 679951f..c8868e1 100644 --- a/SimvaBrowser/KeycloakClient.py +++ b/SimvaBrowser/KeycloakClient.py @@ -29,16 +29,6 @@ def __init__(self, client_secret_file="client_secrets.json", homepage = True): client_id = self.oidc.client_secrets.get('client_id') host=self.oidc.client_secrets.get('host') port=self.oidc.client_secrets.get('port') - #ssl_cert_file=self.oidc.client_secrets.get('ssl_cert_file') - #if(ssl_cert_file): - # self.flaskServer.config.update({ - # 'SSL_CERT_FILE': ssl_cert_file, - # 'SSL_VERIFY': True - # }) - #else: - # self.flaskServer.config.update({ - # 'SSL_VERIFY': False - # }) refereruri=f"https://{host}" if self.oidc.client_secrets.get('secure') == "True" else f"http://{host}:{port}" print(refereruri) self.accountpage=f"{issuer_url}/account?referrer={client_id}&referrer_uri={refereruri}" @@ -118,9 +108,5 @@ def logoutkeycloak(): return redirect(redirect_uri) if __name__ == '__main__': - import requests - # Ensure the certificate is trusted - requests.packages.urllib3.disable_warnings() # Optional, suppress SSL warnings - #os.environ['REQUESTS_CA_BUNDLE'] = '/usr/local/share/ca-certificates/internal-CA.crt' flask = KeycloakClient() flask.flaskServer.run(debug=True, port=5000) \ No newline at end of file