Giter VIP home page Giter VIP logo

jspsychmaker's Introduction

jsPsychMaker

Lifecycle: experimental Codecov test coverage

An set of tools to help create tasks and experimental protocols with jsPsych. It allows you quickly create online or offline protocols, randomizing participants between conditions making sure the groups are balanced, etc.

For details about the available tasks, you can check the Tareas jsPsychR googledoc (SPANISH!), or the jsPsychR-manual.

Installing

Install jsPsychMaker with: remotes::install_github("gorkang/jsPsychMaker")

Creating a protocol from existing tasks

To create a full protocol using already existing tasks:

  1. List available tasks: jsPsychMaker::list_available_tasks()

  2. Create protocol:

jsPsychMaker::create_protocol(canonical_tasks = c("AIM", "EAR", "IRI"),
                              folder_output = "~/Downloads/TEST/new_protocol", 
                              launch_browser = TRUE)

Check if there are new tasks available in a new version of the Github package:

jsPsychMaker::check_NEW_tasks_Github()

Creating a protocol with new tasks

You can create a protocol with new tasks using csv or excel files:

  1. Copy example tasks to local folder: jsPsychMaker::copy_example_tasks(destination_folder = "~/Downloads/TEST/example_tasks")

  2. Create protocol

jsPsychMaker::create_protocol(folder_tasks = "~/Downloads/TEST/example_tasks",
                              folder_output = "~/Downloads/TEST/new_protocol_999", 
                              launch_browser = TRUE)

See the jsPsychR manual, and "~/Downloads/TEST/example_tasks" for examples of how to create new tasks.

Adapting protocols

After creating a protocol, you can edit the config.js file to tweak the configuration. There is a Shiny app to help you create the protocol configuration, and the consent form.

Data preparation

Each of the tasks should have a sister script in jsPsychHelpeR to automatize the data preparation. With jsPsychHelpeR, using a simple function jsPsychHelpeR::run_initial_setup() you can setup a full project to automatically run the full data preparation pipeline for a jsPsychMaker protocol.

HELP with new tasks

If you need help creating a new task, create a new issue and fill the details of the task in the NEW jsPsychR tasks document.

More information

Check the jsPsychR-manual.

jspsychmaker's People

Contributors

gorkang avatar herm4nv avatar

Stargazers

 avatar  avatar

Watchers

 avatar

jspsychmaker's Issues

Nuevo protocolo falla si all_conditions no esta definida

Ocurre en protocolo 10 y 12 con la v0.2.0. NO ocurre en test/10_OLD (tiene alguna version intermedia de la maquinaria)

Primera solucion

  • Una solucion temporal ha sido poner all_conditions = {"protocol": {"type": ["survey"]}}; en el config.js

Dados los problemas con la otra solucion, me inclino mas por una version de la primera (en esencia, hacer un check en helpers. Si existe all_conditions, comprueba si esta bien definido, si no existe, definelo: all_conditions = {"protocol": {"type": ["survey"]}}; )

Segunda solucion

HAY UN PROBLEMA CON ESTA SOLUCION. NO SE ALMACENA NADA EN USER_TASK, POR LO QUE NO SE PUEDE RECUPERAR EL ESTADO DE LOS PARTICIPANTES.

Lineas ~ 312 en mysql_controller.js

// REVIEW: With all_conditions = {}; it will never enter the for loop, and condition_selection() was not giving any response, freezing the protocol
if (unique_between_tasks.length === 0) {

  if (debug_mode === true) console.error('condition_selection() | unique_between_tasks EMPTY | No between_tasks defined');
  resolve(true);

} else {

  // AQUI EL FOR LOOP
  // For each of the between tasks (usually just one)
  for (var i = 0; i < unique_between_tasks.length; i++) {

Esto podria generar algun problema?

Transicion a jsPsych8 - PLUGINS modificados

De cara a la siguiente version del Maker, tenemos que tener en cuenta algunas cosas.

Es importante usar los plugins canonicos siempre que podamos. Cuando no podamos, reducir lo mas posible las modificaciones.

Por ejemplo:

  • Hay alguna manera de capturar el contenido de la pantalla sin modificar el plugin? (ver 1. abajo)
  • Podemos NO usar stripHtml(JSON.stringify())? (ver 2. abajo)

En muchos de los plugins hemos editado manualmente un par de cosas.

  1. Creado questions_list
var questions_list = {}
questions_list["Q".concat(i.toString())] = question.prompt;

Esto permite capturar el stimulus en el csv de salida.

IMPORTANTE: REVISAR que capturamos el contenido de la pantalla en todos los plugins.


  1. Añadido stripHtml(JSON.stringify())
var trialdata = {
          stimulus: stripHtml(JSON.stringify(questions_list)),
          rt: response_time,
          response: stripHtml(JSON.stringify(question_data))
        };

Esto nos obliga a usar JSON.parse() cuando capturamos las respuestas:

let data = (JSON.parse((jsPsych.data.get().values().find(x => x.trialid === 'IfQuestion_001'))['response'])['Q0']).trim();

Sin esto, hay que quitar el JSON.parse():

let data = (((jsPsych.data.get().values().find(x => x.trialid === 'IfQuestion_001'))['response'])['Q0']).trim();

Cambios en IndexedDB_controller

Hay algunas diferencias importantes entre IndexedDB y MySQL

CRITICAS

Cuando un usuario ya asignado retoma un protocolo, se dan estos dos problemas:

  • A veces NO se cargan las tareas ya realizadas a tiempo!!! (VER LINEA ~590 indexedDB_controller.js). Se necesita un LEFT_JOIN o el equivalente ahi!
  • Error FONDECYT.js:95 Uncaught TypeError: Cannot read properties of undefined (reading '0') at FONDECYT.js:95. Esto ocurre en Chrome (no siempre), y no parece ocurrir en Firefox.

He puesto una secciones como la de abajo en indexedDB_controller.js para "arreglar" el problema. Si se comenta LONG_COMPUTATION, aparecen mucho mas frecuentemente los dos errores de arriba. Incluso con LONG_COMPUTATION, en el navegador de los monos (Chrome en docker) aparece el problema muy a menudo.

// DELETEME WHEN FIXED ----------------------
LONG_COMPUTATION = Array.from(Array(50000000).keys());
// END DELETEME ------------------------------

OTRAS

  • Renombrar tablas IndexedDB (e.g. condition deberia ser user_condition?) para que usen los mismos nombres que las tablas MySQL
  • Linea 642 de mysql_controller.js usa Object.keys(between selection).length y 651 de indexedDB_controller.js usa condition_data.length. PQ la diferencia? condition_data.length parece mas bonita :)
  • clean_indexeddb es diferente. Falta DBtime, etc
  • La logica de algunas secciones de indexedDB_controller es distinta a la de MYSQL (por ejemplo Discarded Section in check_id too different (if (actual_user.status == "discarded") {)). TODO lo posible en indexedDB_controller deberia ser IDENTICO a mysql_controller

Functions for common procedures

El procedimiento cuando asignamos un participante a una condicion se repite 2 veces. Deberia ser una funcion?

  • Se podria poner un parametro para añadir o descartar. De ese modo, con una sola funcion cubrimos un buen numero de casos
                // TODO: The user table updates should be a single call (?)
              XMLcall("updateTable", "user", {id: {"id_user": uid}, data: {"status": "assigned"}});
              XMLcall("updateTable", "user", {id: {"id_user": uid}, data: {"start_date": actual_time}});
              XMLcall("updateTable", "protocol", {id: {"id_protocol": pid}, data: {"counter": "counter + 1"}});
              
               // UPDATE: assigned_task + 1 for each between_selection variable
              for (var [key, value] of Object.entries(between_selection)) {
                for (var i = 0; i < value.length; i++) {
                  XMLcall("findRow", "experimental_condition", {keys: ["condition_name"], values: [value[i]]}).then(function(actual_condition) {
                    //if (debug_mode === true) console.warn(new Date().toISOString().slice(0, 19) + " || check_id_status: DISCARDED re-assigned || updateTable: experimental_condition || assigned_task: assigned_task + 1");
                    XMLcall("updateTable", "experimental_condition", {id: {"id_condition": actual_condition.id_condition}, data: {"assigned_task": "assigned_task + 1"}});
                  });
                }
              }
              console.warn('completed_task_storage() | UPDATE | status: assigned, start_date: actual_time, counter + 1, assigned_task + 1 | !user_assigned && !experiment_blocked --> actual_user.status == "discarded" --> !protocol_blocked && accept_discarded');

Varias between condition en la misma tarea?

Probar si funciona el sistema cuando asignamos mas de una between condition en la misma tarea.

Por ejemplo:

BTWN1: [text, pict]
BTWN2: [cancer, sida, fals]

Participant1: text, cancer
Participant2: pict, fals
Participant3: pict, sida

Revisión de sliders

Hay que verificar que no se esté usando required en los sliders de la versión final en cada tarea del canonical_protocol_DEV (sólo es necesario el require_movement)

v0.2 release checklist

TESTS

  • online
    • standard
    • random_id
    • discarded and readmitted (max_time / accept_discarded)
    • return to protocol after refresh (forced_refresh = true)
  • offline
    • standard
    • random_id
    • discarded and readmitted (max_time / accept_discarded)
    • return to protocol after refresh (forced_refresh = true)

RELEASE

  • Update DESCRIPTION AND NEWS in jsPsychMaker, jsPsychMonkeys and jsPsychHelpeR
  • Add messages to IndexedDB #16
  • Copy canonical_protocol_DEV to canonical_protocol
  • Commit and PUSH all changes in jsPsychR-manual, jsPsychMaker, jsPsychMonkeys and jsPsychHelpeR
  • Create release v0.2 in Gthub for jsPsychR-manual, jsPsychMaker, jsPsychMonkeys and jsPsychHelpeR

Cambiar manera en la que se extraen los procedures?

Ahora es necesario poner procedure: XXX en data y en cada question, if_question, timeline, etc.

Esto es la funcion obtain_experiments() de protocol_controller.js extrae las questions buscando procedure.

El problema es que si uno se olvida de insertar el procedure en alguno de los sitios necesarios, la pregunta no aparece.

Si solo fuera necesario meterlo en data data: {trialid: 'DEMOGR_08', procedure: 'DEMOGR'},, seria mas dificil cometer errores.

Abajo una funcion que extrae los procedures de data. No estoy seguro de si funciona con las if_question

// Gets all the procedures from data.

// Works with if_question???
//

for(var i= 0; i < questions.length; i++) {

  if(questions[i].type === "call-function") {

    console.log({type: "call-function", screen: i});

  } else if(questions[i].type === "preload") {

    console.log({type: "preload", screen: i});

  } else if(questions[i].type === "fullscreen") {

    console.log({type: "fullscreen", screen: i});

  } else if(questions[i].timeline !== undefined) {

    // Timeline where data is created by a function
    if (typeof questions[i].timeline[0].data === "function") {

      // TODO: add screen. trialid probably can't
      console.log(questions[i].timeline[0].data.toString().match(/procedure: '.*'/gm));

    } else {

      questions[i].timeline[0].data.screen = i;
      console.log(questions[i].timeline[0].data);

    }

  } else {

      questions[i].data.screen = i;
      console.log(questions[i].data);

  }

 }

Agregar opción para la versión de tarea

Se debe conversar sobre la factibilidad de agregar versiones de tareas con pequeñas modificaciones para los instrumentos, las que quedarían registradas por la versión específica en la función del call_function, usada de la forma: call_function("Consent", "v2");

Actualmente está implementada de forma superficial, ya que todas las versiones han sido "original" (y es lo que dice el csv al crearse el archivo)

Tareas con preguntas llave en otras tareas

Si una tarea tiene una pregunta llave que se muestra fuera de la tarea (e.g. pregunta llave se muestra en DEMOGR, hi se crea una variable en js, y despues la tarea usa esa variable), si los usuarios refrescan el navegador, se pierde la variable.

Solucion1: SIEMPRE incluir las preguntas llave dentro de las pruebas que las requieren
Solucion2: Incluir un campo en la base de datos con un diccionario de variables transversales al protocolo

Aleatorizacion manteniendo N de participantes por grupo equilibrado

Cuando se aleatorizan participantes a condiciones experimentales, es critico que se mantenga equilibrado el numero de participantes por condicion.

Tal vez podriamos registrar en un JSON en la carpeta del servidor cuantos participantes han sido asignados a cada condicion, y cuantos han completado el protocolo en cada condicion?

He creado una discusion en jsPsych para preguntar sobre el tema: jspsych/jsPsych#1719

Orden de tareas

Para ordenar las tareas, tenemos que usar algo que nos de suficiente flexibilidad como para poder implementar todo tipo de ordenes. Tengo la impresion de que lo que hay ahora solo nos permite meter tareas aleatorizadas entre un conjunto de tareas iniciales y alguna final.

No es mejor algo similar a lo de abajo? Tenemos que recuperar el codigo antiguo para generar esto a partir del 'orden_pruebas.csv' (renombremos el archivo 'tasks_order.csv').

Por otro lado, llamemos "tasks" a las tareas (un experiment es, generalmente, un conjunto de tareas).

if (params.has("experiments")) {
  all_tasks = JSON.parse(params.get('experiments')); // example index.html?experiments=["Consent","Goodbye"]
} else {
  var tasks_random = shuffle(['SCSORF','EAR','SASS','PSS','OTRASRELIG','COVIDCONTROL']);
  var all_tasks = ['Consent','DEMOGR3','AIM'] + experiments_random.slice(0,6) + ['Goodbye'];
}

Problemas MAC

Hay que hacer varias pruebas para solucionar problemas en mac OS - Safari, la mayor parte de los problemas encontrados surgen al tratar de entrar con un usuario ya creado, mac entra en lugares a los que no debería entrar, por ejemplo:

Se realizó el cambio el la linea 654 del canonical_protocol_dev de "text_input_uid.innerHTML(..." a "text_input_uid.innerHTML = ...", inicialmente no generaba problemas porque era un caso al cual se supone que no debería llegar el código.

Se generan también problemas al salirse de la pantalla completa (no deja volver al instrumento y se buguea al nivel de que la única forma de solucionarlo es cerrar el navegador)

En la máquina virtual se genera problemas para obtener los audios y videos en mac, no los reconoce (instrumento de ana)

Esos son algunos errores encontrados durante el día de ayer, cuando tengamos un mac en mano creo que habrá que hacer pruebas más exhaustivas

Verificación de versión de SQL call:

el código anteriormente usado era, para el php:
`

function directCall($data, $conn) {

$query = $data["value"];
$result = mysqli_query($conn, $query);
echo('[');
$starting = true;
while ($row = $result->fetch_assoc()) {
  foreach($row as $key=>$value) {
    if ($starting){
      $starting = false;
    } elseif ($starting == false && $key == "task_name") {
      echo ", ";
    }
    echo('{"' . $key . '": "' . $value . '"}');
  }
}
echo(']');

};

`

y para el js:

`

XMLcall("directCall", "", {query: "SELECT task.task_name FROM user LEFT JOIN user_task USING (id_user) LEFT JOIN task USING (id_task) WHERE id_user = " + uid.toString()}).then( function(tasks_list) {

completed_experiments = Array.from(tasks_list, x => x.task_name);

if (debug_mode === true) console.log("check_id_status() [[ completed_experiments: " + completed_experiments.length + " || all_tasks: " + all_tasks.length + " ]]")

// se carga en caso de que el usuario esté asignado
script_loading("tasks", all_tasks, completed_experiments);

});

`

en el de php pensaba agregar la verificación del select despues del "$query = $data["value"];" que sería algo como "if (str_starts_with($query, 'SELECT'))"

PHP bypass

Una de las formas correctas que se me ocurre para general el bypass es simplemente hacer el llamado desde la línea 64 de la función general_query usando:

if (str_starts_with( strtoupper($data["sql"]), 'SELECT')) { ......code....... }

acá se hace la verificación de si la petición pasada a UPPER (select, Select, etc), comienza con SELECT, que debería servirnos para que no se hagan peticiones extrañas, respecto a lo del punto y coma podría agregarse en la linea 234, antes de entrar a las funciones, para que haga una verificación para cualquier función que se reciba, encontré esta función que nos puede servir:

if (strpos($mystring, $word) == false)

que lo que hace es verificar si existe una palabra dentro de otra palabra, entonces verificamos que $word = ";" no exista dentro de $mystring, ahora lo que estoy pensando es que esa verificación hay que hacerla sobre todo el arreglo de $data que se recibe, así que talvez deba ser un for, en caso que sea aceptado puede entrar a las funciones.

Ahí me cuentas que opinas
Saludos

Aleatorizar orden de items en todos los questionarios?

Ahora hay muchas tareas donde NO se aleatoriza el orden de los items. Por ejemplo: MDMQ, ESM, IEC etc.

Por defecto, deberiamos aleatorizar el orden de los items en los cuestionarios. Las excepciones son las tareas experimentales, que tienen su propia logica interna.

Solo habria que añadir:

  • if (debug_mode === false) NAMETASK = jsPsych.randomization.repeat(NAMETASK,1); (#12)

Y quitar los numeros en el texto de los items:

  • Vigilar con aquellas pruebas donde los items tienen "numero", por ejemplo MDMQ (quitar numeros #34)

Crear makers diferentes para surveys y experimentos

Deberiamos crear dos makers serparados (maker_survey.py y maker_experiment.py) para que la complejidad de la creacion de experimentos no afecte a los surveys.

Idealmente, tal vez ambos podrian compartir algunos "modulos".

secuentially_ordered_tasks_n mispelled!

In config.js, the secuentially_ordered_tasks_1 is mispelled

secuentially_ordered_tasks_n SHOULD be sequentially_ordered_tasks_n

Change and make sure the tasks listed there are presented in sequential order.

When making the change, should also change app/app.R:

} else if (name_input == "secuentially_ordered_tasks") {

Dos participantes asignados a la misma condicion al mismo tiempo

Si dos participantes entran al mismo tiempo, el numero de asignados por condicion que leen es el mismo. Esto genera situaciones donde los dos son asignados a la misma condicion y el desequilibrio entre condiciones es > 1.

El proceso es:

  • index.html [first screen]: check_id_status() --> condition_selection() --> ASIGNACION de between_selection
  • complete first task: saveData() --> completed_task_storage() --> WRITE to DB

Esto supone que si la persona 1 se pasa 10 minutos leyendo el consentimiento, durante esos 10 minutos tiene reservada esa condicion!

Potencial solucion

Llamar a condition_selection() en completed_task_storage(), para la asignacion de between_selection en participantes nuevos.

De ese modo la asignacion de between_selection y WRITE to DB estaran juntas.

Evaluación Plugin Survey Text

Se requiere re evaluar el plugin de survey-text por los cambios grandes que se le han hecho, por lo que hay que lanzar monos al compilado de tareas aceptadas actualmente para asegurarse que todas funcionan correctamente,

La nueva versión está actualmente en canonical_protocol_DEV/jsPsych-6/plugins/jspsych-survey-text.js

Una vez probada y validada habría que moverla al canonical protocol y al clean_canonical_protocol

Plugins and how stimulus is stored

Some inconsistencies about how stimulus is stored. Right now:

  • survey-html-form: {"Q0":null}
  • survey-multi-choice-vertical: {"Q0":"Has dicho que la probabilidad es del 3% ¿Recomendarias esta prueba ..... paciente?"}
  • html-slider-response: <div class='justified'>¿Con qué seguridad recomendarías la prueba de <u>screening</u> para la enfermedad cáncer?</div></br>

Fix html-slider-response:

  • html-slider-response: {"Q0":"¿Con qué seguridad recomendarías la prueba de screening para la enfermedad cáncer?"}
  • Clean up html tags and use the {"Q0":""} syntax. Same as survey-multi-choice-vertical does!

Fix survey-html-form:

  • Clean up html tags and insert first 512 characters

Parametro similar a accept_discarded para continuar donde lo dejaron

Ahora mismo, si el participante recarga la web, continua donde lo dejo (reinicia la ultima prueba no completada).

Un problema asociado es que, un participante puede reintentar una tarea con F5.

Deberiamos tener un parametro similar a accept_discarded para continuar o no donde lo dejaron?

  • Es posible mantener un archivo temporal oculto para cada participante donde se guarda el csv de salida linea a linea ? De ese modo, podriamos continuar exactamente donde lo dejaron...

Como ir a una prueba especifica del protocolo via URL?

Implementar parametros en URL que nos permitan ir a una prueba especifica, pregunta, condicion...

Se puede crear un filtro en questions.

IMPORTANTE: Solo deberia funcionar cuando estamos en modo DEBUG.

  • ?task_debug=
  • ?trialid_debug=
  • ?condition_debug=

Usar backtics para html forms, instrucciones, etc.

En JS se pueden usar backtics ` en lugar de simple quotes ', lo que tiene algunas ventajas.

Ver: https://www.jspsych.org/tutorials/rt-task/#part-3-show-instructions y https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

Algo como esto:

var html = '<div class="row" style="display: flex; align-items: center">';
html += '<div class="column" style="float: left; width: 50%">' + "Para detectar " + jsPsych.timelineVariable('disease', true) + "" + data_disease[jsPsych.timelineVariable('disease', true)]["disease_description"] + ", se realiza  " + data_disease[jsPsych.timelineVariable('disease', true)]["test_description"] + ".<BR><BR> La enfermedad tiene una prevalencia de 1 de cada " + data_test_quality[jsPsych.timelineVariable('test_quality', true)]["number_PREVALENCE"] + ". La sensibilidad de la prueba es de " + data_test_quality[jsPsych.timelineVariable('test_quality', true)]["number_SENSITIVITY"] + "%. La especificidad de la prueba es de " + data_test_quality[jsPsych.timelineVariable('test_quality', true)]["number_SPECIFICITY"] + "%. <BR><BR> ¿Cual es la probabilidad de tener la enfermedad si el resultado es positivo?, todo esto está asociado a " + data_test_quality[jsPsych.timelineVariable('test_quality', true)]["text"] + '</div>';

Se es equivalente a esto.

var html = `<div class="row" style="display: flex; align-items: center"><div class="column" style="float: left; width: 50%">
Para detectar ${jsPsych.timelineVariable('disease', true)} ${data_disease[jsPsych.timelineVariable('disease', true)]["disease_description"]},
se realiza ${data_disease[jsPsych.timelineVariable('disease', true)]["test_description"]}.<BR><BR>
La enfermedad tiene una prevalencia de 1 de cada ${data_test_quality[jsPsych.timelineVariable('test_quality', true)]["number_PREVALENCE"]}.
La sensibilidad de la prueba es de ${data_test_quality[jsPsych.timelineVariable('test_quality', true)]["number_SENSITIVITY"]}%.
La especificidad de la prueba es de ${data_test_quality[jsPsych.timelineVariable('test_quality', true)]["number_SPECIFICITY"]}%. <BR><BR>
¿Cual es la probabilidad de tener la enfermedad si el resultado es positivo?, 
todo esto está asociado a ${data_test_quality[jsPsych.timelineVariable('test_quality', true)]["text"]}</div>`

Me parece mucho mas legible y facil de manejar.

¿Que te parece?

Definir ruta a `.secrets_mysql.php` en config.js

Ahora la ruta se define en controllers/php/mysql.php pero es una ruta absoluta.

Deberia ser relativa: ../../.secrets_mysql.php.

Se podria indicar en config.js?
Alternativamente, podriamos crear un config.php?

Mejoras

Protocol_controller.js

Linea 266

Se puede cambiar el bucle por:
SELECT condition_name, task_name FROM user_condition
LEFT JOIN experimental_condition USING (id_condition)
WHERE (id_user = '15');

Linea 36-55

Si es un usuario nuevo, se selecciona la condicion con menos asignados iterativamente.
Se puede cambiar por algo que evite un bucle?

Parametros de preload no funcionan

En protocol_controller.js:

  var preload = {
    type: 'preload',
    show_progress_bar: true,
    message: 'El protocolo está cargando, espere un momento...',
    images: images,
    audio: audio,
    video: video
  }
  questions.unshift({type: 'preload', images: images});

El message no se muestra. Si se cambia show_progress_bar a false, igualmente se muestra...
Si se cambia el valor por defecto de message en el plugin jspsych-preload.js, entonces si se muestra al cargar media.

Store in DB the questions progress

This way we can continue in the exact question where people left the protocol.

Right now they can continue in the task where they left, which means they might repeat some items.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.