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/LoadProcessStatements.py b/LoadProcessStatements.py new file mode 100644 index 0000000..8606ffa --- /dev/null +++ b/LoadProcessStatements.py @@ -0,0 +1,102 @@ +from datetime import datetime +import json +import pandas as pd +import sys +import base64 +from dataclasses import dataclass +import traceback + +@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 + +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) +# 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 +# +# 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.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 + statements=str.splitlines() + for statement in statements: + s=json.loads(statement) + progress.value=count/total + count+=1 + processxapisgdata(s, xapiData) + 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 + processxapisgdata(s, xapiData) + 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.now() - start_time}. Displaying visualizations ...") + +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() + 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)) + sys.exit(-1) + +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).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.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/SimvaBrowser/KeycloakClient.py b/SimvaBrowser/KeycloakClient.py new file mode 100644 index 0000000..c8868e1 --- /dev/null +++ b/SimvaBrowser/KeycloakClient.py @@ -0,0 +1,112 @@ +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') + 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__': + 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-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/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..49453b5 --- /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__, serve_locally=True) +# 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..62d0b7b --- /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, serve_locally=True) + +# 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 new file mode 100644 index 0000000..ae4f25f --- /dev/null +++ b/demo.ipynb @@ -0,0 +1,322 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# cargamos un fichero con trazas, usando código de t-mon\n", + "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": null, + "metadata": {}, + "outputs": [], + "source": [ + "# cargamos pandas, plotly\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)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.json_normalize(xapiData)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# algo de información básica\n", + "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": [ + "## Progress Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "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": [], + "source": [ + "# 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", + "\n", + "## Show the plot\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "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": "markdown", + "metadata": {}, + "source": [ + "### Intentemos ver puntuación en cada completable..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "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 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=\"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": [], + "source": [ + "data = []\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=\"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()" + ] + } + ], + "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/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..6f699ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,9 @@ ipympl nbformat requests boto3 -jwt \ No newline at end of file +jwt +plotly +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"}