diff --git a/LICENCE-THIRD-PARTY b/LICENCE-THIRD-PARTY index 300edb7a7..40e337fff 100644 --- a/LICENCE-THIRD-PARTY +++ b/LICENCE-THIRD-PARTY @@ -65,6 +65,15 @@ licensed under MIT licence. third-party-licences/before-after.txt ============================================================================== +============================================================================== +Bias in Big Data +------------------------------------------------------------------------------ +https://github.com/NCC74656/Bias-In-Big-Data-Interactive +Copyright 2019 Mark Henszey Wolgin +licensed under MIT licence. +third-party-licences/bias-in-big-data.txt +============================================================================== + ============================================================================== big.js ------------------------------------------------------------------------------ @@ -184,12 +193,12 @@ third-party-licences/cytoscape-no-overlap.txt ============================================================================== ============================================================================== -Bias in Big Data +Data Visualization ------------------------------------------------------------------------------ -https://github.com/NCC74656/Bias-In-Big-Data-Interactive -Copyright 2019 Mark Henszey Wolgin +https://github.com/ahallak/CISC374 +Copyright 2019 ahallak, sestrobel, joy-kitson, KMeyerUD. licensed under MIT licence. -third-party-licences/bias-in-big-data.txt +third-party-licences/data-visualization.txt ============================================================================== ============================================================================== diff --git a/csfieldguide/interactives/content/en/interactives.yaml b/csfieldguide/interactives/content/en/interactives.yaml index 13bedddc2..cb35ccedc 100644 --- a/csfieldguide/interactives/content/en/interactives.yaml +++ b/csfieldguide/interactives/content/en/interactives.yaml @@ -48,6 +48,8 @@ confusing-error: name: Confusing Error data-bias: name: Data Bias +data-visualisation: + name: Data Visualisation date-picker: name: Date Picker deceiver: diff --git a/csfieldguide/interactives/content/structure/interactives.yaml b/csfieldguide/interactives/content/structure/interactives.yaml index 4e8805c8a..b84a97cd9 100644 --- a/csfieldguide/interactives/content/structure/interactives.yaml +++ b/csfieldguide/interactives/content/structure/interactives.yaml @@ -115,6 +115,10 @@ data-bias: languages: en: interactives/data-bias.html is_interactive: true +data-visualisation: + languages: + en: interactives/data-visualisation.html + is_interactive: true date-picker: languages: en: interactives/date-picker.html diff --git a/csfieldguide/package.json b/csfieldguide/package.json index b7b4f4318..5c5b6594a 100644 --- a/csfieldguide/package.json +++ b/csfieldguide/package.json @@ -24,6 +24,7 @@ "csfg-interactive-cmy-mixer": "file:./static/interactives/cmy-mixer/", "csfg-interactive-colour-matcher": "file:./static/interactives/colour-matcher/", "csfg-interactive-data-bias": "file:./static/interactives/data-bias/", + "csfg-interactive-data-visualisation": "file:./static/interactives/data-visualisation/", "csfg-interactive-frequency-anaylsis": "file:./static/interactives/frequency-analysis/", "csfg-interactive-jpeg-compression": "file:./static/interactives/jpeg-compression/", "csfg-interactive-matrix-simplifier": "file:./static/interactives/matrix-simplifier/", diff --git a/csfieldguide/static/interactives/data-visualisation/README.md b/csfieldguide/static/interactives/data-visualisation/README.md new file mode 100644 index 000000000..dd631625a --- /dev/null +++ b/csfieldguide/static/interactives/data-visualisation/README.md @@ -0,0 +1,24 @@ +# Data Visualisation interactive + +**Authors:** + +- Amjed Hallak +- sestrobel +- Joy Kitson +- KMeyerUD + +The original can be found [here](https://github.com/ahallak/CISC374). + +**Recreated by:** Alasdair Smith + +This game serves to teach students that visualisation can help a user quickly identify trends in data, and that some visualisations are more helpful than others. + +## The Game + +The user is presented with a data set for a short period, then expected to enter the mode (most common value) of the data. +This process is repeated with different visualisation techniques, including: a grid, heatmap, pie chart and bar chart. + +## Licences + +This interactive uses [Chart.js](https://www.chartjs.org/). +Its licence, and that of the original game, is listed in `LICENCE-THIRD-PARTY`, with a full copy available in the `third-party-licences` directory. diff --git a/csfieldguide/static/interactives/data-visualisation/js/data-visualisation.js b/csfieldguide/static/interactives/data-visualisation/js/data-visualisation.js new file mode 100644 index 000000000..192d1e2df --- /dev/null +++ b/csfieldguide/static/interactives/data-visualisation/js/data-visualisation.js @@ -0,0 +1,494 @@ +const CHART = require('chart.js'); + +const MIN = 0; // Numbers for data +const MAX = 9; +const WAIT = 5000; // Time (milliseconds) to show the chart +const BASE_DATA_POINTS = 16; +const MAX_DATA_POINTS = 256; +const CHART_TYPES = { + GRID: gettext("Plain grid"), + MAP: gettext("Heatmap"), + PIE: gettext("Pie chart"), + BAR: gettext("Bar chart"), +}; +const TITLES = { // Bar/Pie chart titles + PIE: gettext("Relative frequency of each number (%)."), + BAR: gettext("Occurences of each number."), + RESULTS: gettext("Your accuracy (%) when finding the mode of data with different visualisations.") +}; +const COLOURS = [ // Pie chart colours + "#3e95cd", "#8e5ea2", "#3cba9f", "#e8c3b9", "#c45850", + "#28a745","#2a3da0","#34fcfe","#fb8532","#a37533", +]; +const HEATMAP = [ // Heatmap colours + "#0006bf", "#2500c3", "#5200c7", "#8200cb", "#b400cf", + "#d400bf","#d80092","#dc0063","#e00032","#e50000", +]; + +var dataVis = {}; // https://google.github.io/styleguide/javascriptguide.xml#Naming + +dataVis.renderChart; // Chart to be rendered + +dataVis.numberOfDataPoints; // Number of values in the data + +dataVis.typesAndSolutions; // List of lists: [[type of representation, correct answer]] +dataVis.userResponses; // User's choices + +dataVis.currentDataSet; +dataVis.dataFrequencies; // Frequency of each value in dataVis.currentDataSet + +dataVis.chartBar; // Charts +dataVis.chartPie; +dataVis.chartResults; + +dataVis.tableHeadings; // For storing just the headings of the results table + +$(document).ready(function() { + init(); + + $('#button-start').on('click', runStart); + $('#button-submit').on('click', runGuessCheck); + $('#button-next').on('click', runNext); + $('#button-quit').on('click', runQuit); + $('#button-restart').on('click', init); +}); + +/** + * Returns everything to the initial 'page loaded' state + */ +function init() { + showStartScreen(); + + dataVis.renderChart = CHART_TYPES.GRID; + dataVis.numberOfDataPoints = BASE_DATA_POINTS; + dataVis.typesAndSolutions = []; + dataVis.userResponses = []; + dataVis.currentDataSet = []; + dataVis.dataFrequencies = {}; + if (dataVis.tableHeadings) { + $('#data-vis-performance').html(dataVis.tableHeadings); + } else { + dataVis.tableHeadings = $('#data-vis-performance').html(); + } + + newDataSet(); +} + +/** + * Shows the next data representation then, after some time, shows the guess checker + */ +function runStart() { + $('#button-start').addClass('d-none'); + buildChart(); + setTimeout(showGuessScreen, WAIT); +} + +/** + * Runs the guess-checking functionality, then shows the user their performance so far + */ +function runGuessCheck() { + var guess = $('#data-vis-selector').val(); + dataVis.userResponses.push(guess); + if (dataVis.userResponses.length != dataVis.typesAndSolutions.length) { // At this point the two should always be equal + console.log("ERROR: Number of responses does not match number of solutions.") + } + populatePerformanceTable(); + showPerformanceScreen(); +} + +/** + * Prepares the next chart type and dataset + */ +function runNext() { + getNextChart(); + showStartScreen(); + newDataSet(); +} + +/** + * Runs the quit functionality; shows the user their overall performace (results) + */ +function runQuit() { + buildResultsChart(); + showResultsScreen(); +} + +/** + * Assigns the chart type that should be used next, in a circular fashion. + * If the loop has completed, doubles the amount of data to create, up to MAX_DATA_POINTS + */ +function getNextChart() { + if (dataVis.renderChart == CHART_TYPES.GRID) { + dataVis.renderChart = CHART_TYPES.MAP; + } else if (dataVis.renderChart == CHART_TYPES.MAP) { + dataVis.renderChart = CHART_TYPES.PIE; + } else if (dataVis.renderChart == CHART_TYPES.PIE) { + dataVis.renderChart = CHART_TYPES.BAR; + } else /*(dataVis.renderChart == CHART_TYPES.BAR)*/ { + dataVis.numberOfDataPoints = Math.min(MAX_DATA_POINTS, dataVis.numberOfDataPoints * 2); // More data points for the next cycle + dataVis.renderChart = CHART_TYPES.GRID; + } +} + +/** + * Creates the next chart to be displayed + */ +function buildChart() { + $('#data-vis-barchart').addClass('d-none'); + $('#data-vis-piechart').addClass('d-none'); + $('#data-vis-grid').addClass('d-none'); + var type = dataVis.renderChart; + if (type == CHART_TYPES.GRID || type == CHART_TYPES.MAP) { + buildGridChart(); + } else if (type == CHART_TYPES.BAR) { + buildBarChart(); + } else if (type == CHART_TYPES.PIE) { + buildPieChart(); + } + $('#data-vis-game').removeClass('d-none'); +} + +/** + * Builds a grid of values in the data. + * If the representation should be a heatmap, also colours each grid square appropriately + */ +function buildGridChart() { + var numValues = dataVis.currentDataSet.length; + + // We can afford to have fewer columns with a low number of values + var numColumns = numValues > (65) ? (8) : (4); + + var html = ""; + var i=0; + while (i < numValues) { + html += "\n"; + for (var x=0; x < numColumns; x++) { + if (dataVis.renderChart == CHART_TYPES.MAP) { + html += "" + dataVis.currentDataSet[i] + "\n"; + } else { + html += "" + dataVis.currentDataSet[i] + "\n"; + } + i++; + } + html += "\n"; + } + + $('#data-vis-grid').html(html).removeClass('d-none'); +} + +/** + * Builds a pie chart showing the relative frequency of each point in the data + */ +function buildPieChart() { + var valueLabels = []; + var dataPoints = []; + for (var i in dataVis.dataFrequencies) { + if (dataVis.dataFrequencies[i]) { + valueLabels.push(i.toString()); + dataPoints.push(Math.round(dataVis.dataFrequencies[i] / dataVis.currentDataSet.length * 100)); // Percentage of total + } + } + var canvas = $('#data-vis-piechart'); + if (dataVis.chartPie) { + dataVis.chartPie.destroy(); + } + canvas.attr('width', Math.min(600, 0.8 * $( window ).width())); // 600 is the value in the html + canvas.attr('height', canvas.attr('width') / 2); + dataVis.chartPie = new CHART.Chart(canvas, { + type: 'pie', + data: { + labels: valueLabels, + datasets: [{ + backgroundColor: COLOURS, + data: dataPoints + }] + }, + options: { + responsive: true, + legend: { + display: true, + position: 'right' + }, + title: { + display: true, + text: TITLES.PIE + } + } + }); + canvas.removeClass('d-none'); +} + +/** + * Builds a bar chart showing the number of times each value appears in the data + */ +function buildBarChart() { + var valueLabels = []; + var dataPoints = []; + for (var i in dataVis.dataFrequencies) { + valueLabels.push(i.toString()); + dataPoints.push(dataVis.dataFrequencies[i]); + } + var canvas = $('#data-vis-barchart'); + if (dataVis.chartBar) { + dataVis.chartBar.destroy(); + } + canvas.attr('width', Math.min(800, 0.8 * $( window ).width())); // 800 is the value in the html + canvas.attr('height', canvas.attr('width') / 2); + dataVis.chartBar = new CHART.Chart(canvas, { + type: 'bar', + data: { + labels: valueLabels, + datasets: [{ + label: gettext("Occurences"), + backgroundColor: '#2a3da0', // Primary CSFG colour + data: dataPoints + }] + }, + options: { + responsive: true, + legend: { + display: false + }, + title: { + display: true, + text: TITLES.BAR + } + } + }); + canvas.removeClass('d-none'); +} + +/** + * Builds a bar chart showing the user's accuracy when finding the mode for each representation of data + */ +function buildResultsChart() { + var valueLabels = [CHART_TYPES.GRID, CHART_TYPES.MAP, CHART_TYPES.PIE, CHART_TYPES.BAR]; + var dataPoints = getFinalResults(); + var canvas = $('#data-vis-results-chart'); + if (dataVis.chartResults) { + dataVis.chartResults.destroy(); + } + canvas.attr('width', Math.min(800, 0.8 * $( window ).width())); // 800 is the value in the html + canvas.attr('height', canvas.attr('width') / 2); + dataVis.chartResults = new CHART.Chart(canvas, { + type: 'bar', + data: { + labels: valueLabels, + datasets: [{ + label: gettext("Success (%)"), + backgroundColor: '#2a3da0', // Primary CSFG colour + data: dataPoints + }] + }, + options: { + responsive: true, + legend: { + display: false + }, + scales: { + yAxes: [{ + display: true, + ticks: { + beginAtZero: true, + stepValue: 10, + max: 100 + } + }] + }, + title: { + display: true, + text: TITLES.RESULTS + } + } + }); + canvas.removeClass('d-none'); +} + +function populatePerformanceTable() { + var table = $('#data-vis-performance'); + var data = [last(dataVis.typesAndSolutions), last(dataVis.userResponses)]; // [[type, solution], response] + var newRow = "\n" + + "" + data[0][0] + "\n" + + "" + data[0][1] + "\n" + + "" + data[1] + "\n" + + "\n"; + table.append(newRow); +} + +/** + * Returns a list of four items: + * The percentage of correct answers for the mode of each data visualisation type + * + * [grid, heatmap, pie, bar] + */ +function getFinalResults() { + var localSolutions = dataVis.typesAndSolutions; // List of [[type, solution]] + var localResponses = dataVis.userResponses; // List of [response] + var proportionGrid = [0, 0]; // [correct, out-of] + var proportionHeatmap = [0, 0]; + var proportionPie = [0, 0]; + var proportionBar = [0, 0]; + for (var i=0; i < localSolutions.length; i++) { + if (localSolutions[i][0] == CHART_TYPES.GRID) { + if (localSolutions[i][1] == localResponses[i]) { + proportionGrid[0]++; + } + proportionGrid[1]++; + } else if (localSolutions[i][0] == CHART_TYPES.MAP) { + if (localSolutions[i][1] == localResponses[i]) { + proportionHeatmap[0]++; + } + proportionHeatmap[1]++; + } else if (localSolutions[i][0] == CHART_TYPES.PIE) { + if (localSolutions[i][1] == localResponses[i]) { + proportionPie[0]++; + } + proportionPie[1]++; + } else if (localSolutions[i][0] == CHART_TYPES.BAR) { + if (localSolutions[i][1] == localResponses[i]) { + proportionBar[0]++; + } + proportionBar[1]++; + } + } + return [ + Math.round(proportionGrid[0] / proportionGrid[1] * 100), + Math.round(proportionHeatmap[0] / proportionHeatmap[1] * 100), + Math.round(proportionPie[0] / proportionPie[1] * 100), + Math.round(proportionBar[0] / proportionBar[1] * 100) + ]; +} + +/** + * Prepares a new set of data for the next chart. + * Also sets the next solution appropriately. + */ +function newDataSet() { + fillDataSet(dataVis.numberOfDataPoints); + var [solution, frequencies] = getFrequencies(); + dataVis.typesAndSolutions.push([dataVis.renderChart, solution]) + dataVis.dataFrequencies = frequencies; +} + +/** + * Sets the 'dataVis.currentDataSet' global to a list of the given number of random values. + */ +function fillDataSet(num) { + var newDataSet = []; + for (var i=0; i < num; i++) { + newDataSet.push(getRandomInteger(MIN, MAX)) + } + + dataVis.currentDataSet = newDataSet; +} + +/** + * Returns two values in a list: + * [ + * The mode (most common value) of the global dataVis.currentDataSet - ensuring only one value is most common + * , + * The frequency of each value in the global dataVis.currentDataSet + * ] + * + * If two or more values share the mode (most common value), one of the mode values is chosen at random. + * A random datapoint that is not that value is replaced with that value, resulting in a singular mode value. + */ +function getFrequencies() { + // Get the mode(s) + // Based on parts of https://stackoverflow.com/questions/3783950/get-the-item-that-appears-the-most-times-in-an-array + var modes = []; + var frequencies = {}; + var max = 0; + var localDataSet = dataVis.currentDataSet; // Store a copy locally for performance + for (var v in localDataSet) { + frequencies[localDataSet[v]] = (frequencies[localDataSet[v]] || 0) + 1; + if (frequencies[localDataSet[v]] == max) { // More than one current mode + modes.push(localDataSet[v]); + } else if (frequencies[localDataSet[v]] > max) { // New (singular) mode + modes = [localDataSet[v]]; + max = frequencies[localDataSet[v]]; + } + } + + // Deal with conflicts + var mode; + if (modes.length > 1) { + mode = modes[getRandomInteger(0, modes.length - 1)]; // Choose a random best mode + var randomValue = getRandomInteger(0, localDataSet.length); + while (localDataSet[randomValue] == mode) { // Find a random not-best-mode number + randomValue++; + } + frequencies[localDataSet[randomValue]]--; // Reduce the frequecy of that number + dataVis.currentDataSet[randomValue] = mode; // Set best mode (actual dataVis.currentDataSet) + frequencies[localDataSet[randomValue]]++; // Increase the frequency of best-mode + } else { + mode = modes[0]; + } + + // Return the mode and frequencies + return [mode, frequencies]; +} + +/** + * Returns a random integer between min and max inclusive. + * From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + */ +function getRandomInteger(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Returns the last item in the array, like the python expression: `array[-1]` + */ +function last(array) { + return array[array.length - 1]; +} + +/** + * Hides irrrelevant HTML divs, then reveals relevant ones. + */ +function showStartScreen() { + $('#data-vis-guesser').addClass('d-none'); + $('#data-vis-next').addClass('d-none'); + $('#data-vis-restart').addClass('d-none'); + $('#data-vis-game').addClass('d-none'); + $('#data-vis-results').addClass('d-none'); + + $('#data-vis-instructions').removeClass('d-none'); + $('#button-start').removeClass('d-none'); +} + +/** + * Hides irrrelevant HTML divs, then reveals relevant ones. + */ +function showGuessScreen() { + $('#data-vis-instructions').addClass('d-none'); + $('#data-vis-game').addClass('d-none'); + + $('#data-vis-guesser').removeClass('d-none'); +} + +/** + * Hides irrrelevant HTML divs, then reveals relevant ones. + */ +function showPerformanceScreen() { + $('#data-vis-guesser').addClass('d-none'); + $('#data-vis-restart').addClass('d-none'); + $('#data-vis-game').addClass('d-none'); + $('#data-vis-results-chart').addClass('d-none'); + + $('#data-vis-next').removeClass('d-none'); + $('#data-vis-performance').removeClass('d-none'); + $('#data-vis-results').removeClass('d-none'); +} + +/** + * Hides irrrelevant HTML divs, then reveals relevant ones. + */ +function showResultsScreen() { + $('#data-vis-next').addClass('d-none'); + $('#data-vis-performance').addClass('d-none'); + + $('#data-vis-restart').removeClass('d-none'); + $('#data-vis-results-chart').removeClass('d-none'); +} diff --git a/csfieldguide/static/interactives/data-visualisation/package.json b/csfieldguide/static/interactives/data-visualisation/package.json new file mode 100644 index 000000000..abc1ac3d8 --- /dev/null +++ b/csfieldguide/static/interactives/data-visualisation/package.json @@ -0,0 +1,8 @@ +{ + "name": "csfg-interactive-data-visualisation", + "version": "1.0.0", + "private": true, + "dependencies": { + "chart.js": "2.9.1" + } +} diff --git a/csfieldguide/static/interactives/data-visualisation/scss/data-visualisation.scss b/csfieldguide/static/interactives/data-visualisation/scss/data-visualisation.scss new file mode 100644 index 000000000..2e38258c8 --- /dev/null +++ b/csfieldguide/static/interactives/data-visualisation/scss/data-visualisation.scss @@ -0,0 +1,32 @@ +@import "node_modules/bootstrap/scss/functions"; +@import "node_modules/bootstrap/scss/variables"; +@import "node_modules/bootstrap/scss/mixins"; + +canvas { + margin: auto; +} + +table, th, td { + margin: auto; + font-family: monospace; + text-align: center; + padding: 1rem; + padding-left: 3rem; + padding-right: 3rem; + border: 1px solid #000; + border-collapse: collapse; +} + +#data-vis-results, #data-vis-game { + position: relative; + margin: auto; + max-width: 800px; +} + +@include media-breakpoint-down(md) { + table, th, td { + padding: 0.3rem; + padding-left: 1rem; + padding-right: 1rem; + } +} diff --git a/csfieldguide/templates/appendices/contributors.html b/csfieldguide/templates/appendices/contributors.html index 59b0d31db..6c0b1ee9b 100644 --- a/csfieldguide/templates/appendices/contributors.html +++ b/csfieldguide/templates/appendices/contributors.html @@ -139,6 +139,10 @@

{% trans 'Comm
  • gmohler213 (Greg Mohler)
  • Rachel Muzzelo
  • aenkirch
  • +
  • ahallak (Amjed Hallak)
  • +
  • sestrobel
  • +
  • joy-kitson (Joy Kitson)
  • +
  • KMeyerUD
  • sdigiro (Sofia DiGirolamo)
  • mkong001 (Minji Kong)
  • koreymitchell (Korey Mitchell)
  • diff --git a/csfieldguide/templates/interactives/data-visualisation.html b/csfieldguide/templates/interactives/data-visualisation.html new file mode 100644 index 000000000..351d8f867 --- /dev/null +++ b/csfieldguide/templates/interactives/data-visualisation.html @@ -0,0 +1,67 @@ +{% extends interactive_mode_template %} + +{% load i18n %} +{% load static %} + +{% block html %} +
    +

    {% trans 'Data Visualisation' %}

    +
    +
    + {% blocktrans %} + Find the mode (most common number) of the data below. + You will have a limited amount of time to view the data. + {% endblocktrans %} +
    + +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + +
    +
    +
    + + +
    +
    + + + + + + +
    {% trans 'Chart type' %}{% trans 'Mode (most common value) of data' %}{% trans 'Your answer' %}
    + +
    +
    +{% endblock html %} + +{% block css %} + +{% endblock css %} + +{% block js %} + +{% endblock js %} diff --git a/third-party-licences/data-visualization.txt b/third-party-licences/data-visualization.txt new file mode 100644 index 000000000..856927d1c --- /dev/null +++ b/third-party-licences/data-visualization.txt @@ -0,0 +1,10 @@ + +MIT License + +Copyright (c) 2019 ahallak, sestrobel, joy-kitson, KMeyerUD + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.