<commit_before>var questionsArray = [];
var promptArray = ['What is the return value of wallOne?', 'What\'s the importance of updateMessage(); ?', 'Which is correct about the following diagram?', 'What is the name of the folder that the stylesheet is saved in?', 'What would the method return if the value of name was changed?', 'Which method converts a number into a string, keeping a specified number of decimals?', 'Which question refers to the html element using the built in document method?', 'What is true about key value pairs?', 'which of these returns the largest integer less than or equal to a given number?', 'If hourNow = 14, what will happen?', 'which element id is being updated with .textContent?' ];
var responseArray = [['15', '40', '8', 'area'],['It updates the variable el', 'It calls the function', 'It is not important', 'It writes text content into the child element'], ['<p> is a child of <div>', '<head> is a grandchild of document', '<html> is a parent of both <head> and <body>', 'all of these are correct'], ['<link>', 'css', 'index.html', 'it auto links'],['It would not affect how the method calculates the return value', 'checkAvailability will not run properly', 'the value of Quay will still print out', 'You would have to delete the entire Object to rename it'], ['isNan', 'toFixed', 'toPrecision', 'toExponential'], ['getElementById', 'checkAvailability', 'Hotel(name, rooms, booked)', '.name'], ['a unique identifier with data attached', 'it is frequently used to change methods using jQuery', 'they do not have them in any other programming language', 'a deprecated version of control flow'], ['only used in object literals', 'only used in object constructors', 'a unique identifier with data attached', 'no one reall knows'], ['Math.ceil()', 'Math.floor()', 'Math.round()', 'Math.random'], ['the user will see a welcome text', 'the user will see a good morning text', 'the user will see a good evening text', 'the user will see a good afternoon text'], ['var el', 'cost', 'total', 'document']];

var correctAnswer = ['15', 'It calls the function', 'all of these are correct', 'css/', 'It would not affect how the method calculates the return value', 'It would not affect how the method calculates the return value', 'toFixed', 'a unique identifier with data attached', 'Math.floor()', 'the user will see a good afternoon text', 'cost'];
var questionsObject = [];
var answerArray = [];
var questionNames = ['calculate', 'callQuestion', 'htmlQuestion', 'testQuestion', 'varQuestion', 'childQuestion', 'methodQuestion', 'methodQuestion2', 'multipleObjects', 'objLitQuestion', 'returnQuestion', 'roundDownQuestion'];

function Question(name, path) {
  this.name = name;
  this.path = path;
  questionsArray.push(this);
}

(function addingPhotos() {
  for (var i = 0; i < questionNames.length; i++) {
    new Question(questionNames[i], 'questions/' + questionNames[i] + '.png');
  }
})();

function QuesConstructor(quest, answer, allQuestions) {
  this.quest = quest;
  this.answer = answer;
  this.allQuestions = allQuestions;
  // this.canvas = canvas;
  questionsObject.push(this);
};

function addingQuestions() {
  for(var j = 0; j < promptArray.length; j++) {
    new QuesConstructor(promptArray[j], correctAnswer[j], responseArray[j]);
  }
};
addingQuestions();

QuesConstructor.prototype.appendingImage = function() {
  var elQuesPic = document.getElementById('img');
  var elQuesParent = document.getElementById('test');
  var elQuesText = document.createElement('p');
  for(var i = 0; i < questionsArray.length; i++) {
    elQuesPic.src = questionsArray[i].path;
    console.log('image pop');
    elQuesText.textContent = 'hola fucker';
    elQuesParent.appendChild(elQuesText);
  }
};
questionsObject[0].appendingImage();

//display answers
function createRadioButtonFromArray(array) {
  var form = document.getElementById('fromId');
  for(var i = 0; i < responseArray.length; i++) {
    var radio = document.createElement('input');
    radio.type = 'radio';
    radio.name = 'answers';
    radio.class = 'radioButt';
    radio.value = i;
    radio.id = 'radio' + i;
    form.appendChild(radio);

    document.getElementById('radio' + i).textContent = responseArray[i];
  }
};

//display wrong answers in wrong anwser section

//sync submit button to hangman and wrong anwser section

//sync 'back to questions' button back to game and clear wrong answers section

// tracker of wrong and correct answers sync to localstorage

//canvas
var canvas = document.getElementById('canvasHangman'),context = canvas.getContext('2d');
window.onload = function(){
  canvasRender.gallows();
  canvasRender.head();
  canvasRender.torso();
  canvasRender.rightLeg();
  canvasRender.leftLeg();
  canvasRender.rightArm();
  canvasRender.leftArm();
};

var canvasRender = {
  gallows: function(){
    context.fillStyle = 'black';
    context.fillRect(0, 0, canvas.width, canvas.height);

    context.strokeStyle = 'white';
    context.lineWidth = 8.4;
    context.strokeRect(10, 370, 380, 0);

    context.strokeStyle = 'white';
    context.lineWidth = 10;
    context.strokeRect(350, 89, 0, 280);

    context.strokeStyle = 'white';
    context.lineWidth = 10;
    context.strokeRect(200, 100, 160, 0);

    context.strokeStyle = 'white';
    context.lineWidth = 7;
    context.strokeRect(220,100,0,50);
  // angle support
    // context.strokeStyle = 'white';
    // context.lineWidth = 10;
    // context.rotate(Math.PI / 4);
    // context.strokeRect(295, -155, 46, 0);
  },

  head: function() {
    //oval head
    var centerX = 60;
    var centerY = -50;
    var radius = 50;

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(.35, .45);
    context.beginPath();
    context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
    context.restore();

    context.fillStyle = 'white';
    context.fill();
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
  //left eyes
    var eyeX = 150;
    var eyeY = -260;
    var eyeRadius = 50;

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(.07, .09);
    context.beginPath();
    context.arc(eyeX, eyeY, eyeRadius, 0, 2 * Math.PI, false);
    context.restore();

    context.fillStyle = 'black';
    context.fill();
    context.lineWidth = 2;
    context.strokeStyle = 'black';
    context.stroke();
  //right eye
    var rightEyeX = 420;
    var rightEyeY = -260;
    var rightEyeRadius = 50;

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(.07, .09);
    context.beginPath();
    context.arc(rightEyeX, rightEyeY, rightEyeRadius, 0, 2 * Math.PI, false);
    context.restore();

    context.fillStyle = 'black';
    context.fill();
    context.lineWidth = 2;
    context.strokeStyle = 'black';
    context.stroke();
  },

  torso: function() {
    //torso
    context.strokeStyle = 'white';
    context.lineWidth = 8;
    context.strokeRect(221, 205, 0, 79);
  },

  rightLeg: function() {
    //right leg
    var xRightLeg = canvas.width / 2.33;
    var yRightLeg = canvas.height / 1.16;
    var legRadius = 85;
    var startAngle = 1.7 * Math.PI;
    var endAngle = 0 * Math.PI;
    var counterClockwise = false;
    context.beginPath();
    context.arc(xRightLeg, yRightLeg, legRadius, startAngle, endAngle, counterClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
    //right foot
    context.strokeStyle = 'white';
    context.lineWidth = 8;
    context.strokeRect(250, 350, 20, 0);
  },

  leftLeg: function() {
    //legs left leg
    var xleftLeg = canvas.width / 1.48;
    var yleftLeg = canvas.height / 1.16;
    var legRadius = 85;
    var startAngle = 1 * Math.PI;
    var endAngle = 1.3 * Math.PI;
    var counterClockwise = false;
    context.beginPath();
    context.arc(xleftLeg, yleftLeg, legRadius, startAngle, endAngle, counterClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
    //left footer
    context.strokeStyle = 'white';
    context.lineWidth = 8;
    context.strokeRect(170, 350, 20, 0);
  },
  rightArm: function() {
    //right arm
    var xRightArm = canvas.width / 2.31;
    var yRightArm = canvas.height / 1.4;
    var armRadius = 85;
    var startArmAngle = 1.7 * Math.PI;
    var endArmAngle =  -.1 * Math.PI;
    var counterArmClockwise = false;
    context.beginPath();
    context.arc(xRightArm, yRightArm, armRadius, startArmAngle, endArmAngle, counterArmClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
  },
  leftArm: function() {
    var xLeftArm = canvas.width / 1.49;
    var yLeftArm = canvas.height / 1.4;
    var armLeftRadius = 85;
    var startLeftArmAngle = 1.1 * Math.PI;
    var endLeftArmAngle =  1.3 * Math.PI;
    var counterLeftArmClockwise = false;
    context.beginPath();
    context.arc(xLeftArm, yLeftArm, armLeftRadius, startLeftArmAngle, endLeftArmAngle, counterLeftArmClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
  }
};
<commit_msg>made a for loop within a for loop for the resopnsearray<commit_after>var questionsArray = [];
var promptArray = ['What is the return value of wallOne?', 'What\'s the importance of updateMessage(); ?', 'Which is correct about the following diagram?', 'What is the name of the folder that the stylesheet is saved in?', 'What would the method return if the value of name was changed?', 'Which method converts a number into a string, keeping a specified number of decimals?', 'Which question refers to the html element using the built in document method?', 'What is true about key value pairs?', 'which of these returns the largest integer less than or equal to a given number?', 'If hourNow = 14, what will happen?', 'which element id is being updated with .textContent?' ];

var responseArray = [['15', '40', '8', 'area'],['It updates the variable el', 'It calls the function', 'It is not important', 'It writes text content into the child element'], ['<p> is a child of <div>', '<head> is a grandchild of document', '<html> is a parent of both <head> and <body>', 'all of these are correct'], ['<link>', 'css', 'index.html', 'it auto links'],['It would not affect how the method calculates the return value', 'checkAvailability will not run properly', 'the value of Quay will still print out', 'You would have to delete the entire Object to rename it'], ['isNan', 'toFixed', 'toPrecision', 'toExponential'], ['getElementById', 'checkAvailability', 'Hotel(name, rooms, booked)', '.name'], ['a unique identifier with data attached', 'it is frequently used to change methods using jQuery', 'they do not have them in any other programming language', 'a deprecated version of control flow'], ['only used in object literals', 'only used in object constructors', 'a unique identifier with data attached', 'no one reall knows'], ['Math.ceil()', 'Math.floor()', 'Math.round()', 'Math.random'], ['the user will see a welcome text', 'the user will see a good morning text', 'the user will see a good evening text', 'the user will see a good afternoon text'], ['var el', 'cost', 'total', 'document']];

var correctAnswer = ['15', 'It calls the function', 'all of these are correct', 'css/', 'It would not affect how the method calculates the return value', 'It would not affect how the method calculates the return value', 'toFixed', 'a unique identifier with data attached', 'Math.floor()', 'the user will see a good afternoon text', 'cost'];
var questionsObject = [];
var answerArray = [];
var questionNames = ['calculate', 'callQuestion', 'htmlQuestion', 'testQuestion', 'varQuestion', 'childQuestion', 'methodQuestion', 'methodQuestion2', 'multipleObjects', 'objLitQuestion', 'returnQuestion', 'roundDownQuestion'];

function Question(name, path) {
  this.name = name;
  this.path = path;
  questionsArray.push(this);
}

(function addingPhotos() {
  for (var i = 0; i < questionNames.length; i++) {
    new Question(questionNames[i], 'questions/' + questionNames[i] + '.png');
  }
})();

function QuesConstructor(quest, answer, allQuestions) {
  this.quest = quest;
  this.answer = answer;
  this.allQuestions = allQuestions;
  // this.canvas = canvas;
  questionsObject.push(this);
};

function addingQuestions() {
  for(var j = 0; j < promptArray.length; j++) {
    new QuesConstructor(promptArray[j], correctAnswer[j], responseArray[j]);
  }
};
addingQuestions();

QuesConstructor.prototype.appendingImage = function() {
  var elQuesPic = document.getElementById('img');
  var elQuesParent = document.getElementById('test');
  var elQuesText = document.createElement('p');
  for(var i = 0; i < questionsArray.length; i++) {
    elQuesPic.src = questionsArray[i].path;
    console.log('image pop');
    elQuesText.textContent = 'hola fucker';
    elQuesParent.appendChild(elQuesText);
  }
};
questionsObject[0].appendingImage();

//display answers
// var multAnswers = function(arr) {
//   if( typeof(arr) === 'object'){
//     for (var i = 0; i < arr.length; i++){
//       multAnswers(arr[i]);
//       var answerArray = document.getElementById('testOne');
//     }
//   }
// };
// answerArray.appendChild(multAnswers[1](responseArray[1]));

for (var i = 0; i < responseArray.length; i++){
  for (var j = 0; j < responseArray[i].length; j++){
    console.log(responseArray[i][j]);
    // var radioLabels = document.getElementByTagName('label');
    // multAnswers.appendChild(radioLabels);
  }
}

//display wrong answers in wrong anwser section

//sync submit button to hangman and wrong anwser section

//sync 'back to questions' button back to game and clear wrong answers section

// tracker of wrong and correct answers sync to localstorage

//canvas
var canvas = document.getElementById('canvasHangman'),context = canvas.getContext('2d');
window.onload = function(){
  canvasRender.gallows();
  canvasRender.head();
  canvasRender.torso();
  canvasRender.rightLeg();
  canvasRender.leftLeg();
  canvasRender.rightArm();
  canvasRender.leftArm();
};

var canvasRender = {
  gallows: function(){
    context.fillStyle = 'black';
    context.fillRect(0, 0, canvas.width, canvas.height);

    context.strokeStyle = 'white';
    context.lineWidth = 8.4;
    context.strokeRect(10, 370, 380, 0);

    context.strokeStyle = 'white';
    context.lineWidth = 10;
    context.strokeRect(350, 89, 0, 280);

    context.strokeStyle = 'white';
    context.lineWidth = 10;
    context.strokeRect(200, 100, 160, 0);

    context.strokeStyle = 'white';
    context.lineWidth = 7;
    context.strokeRect(220,100,0,50);
  // angle support
    // context.strokeStyle = 'white';
    // context.lineWidth = 10;
    // context.rotate(Math.PI / 4);
    // context.strokeRect(295, -155, 46, 0);
  },

  head: function() {
    //oval head
    var centerX = 60;
    var centerY = -50;
    var radius = 50;

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(.35, .45);
    context.beginPath();
    context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
    context.restore();

    context.fillStyle = 'white';
    context.fill();
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
  //left eyes
    var eyeX = 150;
    var eyeY = -260;
    var eyeRadius = 50;

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(.07, .09);
    context.beginPath();
    context.arc(eyeX, eyeY, eyeRadius, 0, 2 * Math.PI, false);
    context.restore();

    context.fillStyle = 'black';
    context.fill();
    context.lineWidth = 2;
    context.strokeStyle = 'black';
    context.stroke();
  //right eye
    var rightEyeX = 420;
    var rightEyeY = -260;
    var rightEyeRadius = 50;

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(.07, .09);
    context.beginPath();
    context.arc(rightEyeX, rightEyeY, rightEyeRadius, 0, 2 * Math.PI, false);
    context.restore();

    context.fillStyle = 'black';
    context.fill();
    context.lineWidth = 2;
    context.strokeStyle = 'black';
    context.stroke();
  },

  torso: function() {
    //torso
    context.strokeStyle = 'white';
    context.lineWidth = 8;
    context.strokeRect(221, 205, 0, 79);
  },

  rightLeg: function() {
    //right leg
    var xRightLeg = canvas.width / 2.33;
    var yRightLeg = canvas.height / 1.16;
    var legRadius = 85;
    var startAngle = 1.7 * Math.PI;
    var endAngle = 0 * Math.PI;
    var counterClockwise = false;
    context.beginPath();
    context.arc(xRightLeg, yRightLeg, legRadius, startAngle, endAngle, counterClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
    //right foot
    context.strokeStyle = 'white';
    context.lineWidth = 8;
    context.strokeRect(250, 350, 20, 0);
  },

  leftLeg: function() {
    //legs left leg
    var xleftLeg = canvas.width / 1.48;
    var yleftLeg = canvas.height / 1.16;
    var legRadius = 85;
    var startAngle = 1 * Math.PI;
    var endAngle = 1.3 * Math.PI;
    var counterClockwise = false;
    context.beginPath();
    context.arc(xleftLeg, yleftLeg, legRadius, startAngle, endAngle, counterClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
    //left footer
    context.strokeStyle = 'white';
    context.lineWidth = 8;
    context.strokeRect(170, 350, 20, 0);
  },
  rightArm: function() {
    //right arm
    var xRightArm = canvas.width / 2.31;
    var yRightArm = canvas.height / 1.4;
    var armRadius = 85;
    var startArmAngle = 1.7 * Math.PI;
    var endArmAngle =  -.1 * Math.PI;
    var counterArmClockwise = false;
    context.beginPath();
    context.arc(xRightArm, yRightArm, armRadius, startArmAngle, endArmAngle, counterArmClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
  },
  leftArm: function() {
    var xLeftArm = canvas.width / 1.49;
    var yLeftArm = canvas.height / 1.4;
    var armLeftRadius = 85;
    var startLeftArmAngle = 1.1 * Math.PI;
    var endLeftArmAngle =  1.3 * Math.PI;
    var counterLeftArmClockwise = false;
    context.beginPath();
    context.arc(xLeftArm, yLeftArm, armLeftRadius, startLeftArmAngle, endLeftArmAngle, counterLeftArmClockwise);
    context.lineWidth = 8;
    context.strokeStyle = 'white';
    context.stroke();
  }
};
<|endoftext|>


<commit_before>const fs = require('fs');
const mkdirpSync = require('mkdirp').sync;
const path = require('path');
const EventEmitter = require('events');
const Partition = require('./Partition');
const Index = require('./Index');

const DEFAULT_READ_BUFFER_SIZE = 4 * 1024;
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;

/**
 * An append-only storage with highly performant positional range scans.
 * It's highly optimized for an event-store and hence does not support compaction or data-rewrite, nor any querying
 */
class Storage extends EventEmitter {

    /**
     * Config options:
     *   - serializer: A serializer object with methods serialize(document) and deserialize(data). Default is JSON.stringify/parse.
     *   - dataDirectory: The path where the storage data should reside. Default '.'.
     *   - indexDirectory: The path where the indexes should be stored. Defaults to dataDirectory.
     *   - indexFile: The name of the primary index. Default '{storageName}.index'.
     *   - readBufferSize: Size of the read buffer in bytes. Default 4096.
     *   - writeBufferSize: Size of the write buffer in bytes. Default 16384.
     *   - maxWriteBufferDocuments: How many documents to have in the write buffer at max. 0 means as much as possible. Default 0.
     *   - syncOnFlush: If fsync should be called on write buffer flush. Set this if you need strict durability. Defaults to false.
     *   - partitioner: A function that takes a document and sequence number and returns a partition name that the document should be stored in. Defaults to write all documents to the primary partition.
     *   - indexOptions: An options object that should be passed to all indexes on construction.
     *
     * @param {string} [storageName] The name of the storage.
     * @param {Object} [config] An object with storage parameters.
     */
    constructor(storageName = 'storage', config = {}) {
        super();
        if (typeof storageName === 'object') {
            config = storageName;
            storageName = undefined;
        }

        this.storageFile = storageName || 'storage';
        let defaults = {
            serializer: { serialize: JSON.stringify, deserialize: JSON.parse },
            partitioner: (document, number) => '',
            dataDirectory: '.',
            indexFile: this.storageFile + '.index',
            indexOptions: {},
        };
        config = Object.assign(defaults, config);
        this.serializer = config.serializer;
        this.partitioner = config.partitioner;

        this.dataDirectory = path.resolve(config.dataDirectory);
        if (!fs.existsSync(this.dataDirectory)) {
            mkdirpSync(this.dataDirectory);
        }

        this.indexDirectory = path.resolve(config.indexDirectory || this.dataDirectory);

        this.indexOptions = config.indexOptions;
        this.indexOptions.dataDirectory = this.indexDirectory;
        this.index = new Index(config.indexFile, this.indexOptions);
        this.secondaryIndexes = {};

        this.scanPartitions(config);
    }

    /**
     * The amount of documents in the storage.
     * @returns {number}
     */
    get length() {
        return this.index.length;
    }

    /**
     * Scan the data directory for all existing partitions.
     * Every file beginning with the storageFile name is considered a partition.
     *
     * @private
     * @param {Object} config The configuration object containing options for the partitions.
     * @returns void
     */
    scanPartitions(config) {
        let defaults = {
            readBufferSize: DEFAULT_READ_BUFFER_SIZE,
            writeBufferSize: DEFAULT_WRITE_BUFFER_SIZE,
            maxWriteBufferDocuments: 0,
            syncOnFlush: false
        };
        this.partitionConfig = Object.assign(defaults, config);
        this.partitions = {};

        let files = fs.readdirSync(this.dataDirectory);
        for (let file of files) {
            if (file.substr(-6) === '.index') continue;
            if (file.substr(-7) === '.branch') continue;
            if (file.substr(0, this.storageFile.length) === this.storageFile) {
                //console.log('Found existing partition', file);
                let partition = new Partition(file, this.partitionConfig);
                this.partitions[partition.id] = partition;
            }
        }
    }

    /**
     * Open the storage and indexes and create read and write buffers.
     * Will emit an 'opened' event if finished.
     *
     * @api
     * @returns {boolean}
     */
    open() {
        this.index.open();

        // TODO: Open secondary indexes lazily
        this.forEachSecondaryIndex(index => index.open());

        this.emit('opened');
        return true;
    }

    /**
     * Close the storage and frees up all resources.
     * Will emit a 'closed' event when finished.
     *
     * @api
     * @returns void
     */
    close() {
        this.index.close();
        this.forEachSecondaryIndex(index => index.close());
        this.forEachPartition(partition => partition.close());
        this.emit('closed');
    }

    /**
     * Flush the write buffer to disk.
     * This is a sync method and will invoke all previously registered flush callbacks.
     * Will emit a 'flush' event when done.
     *
     * @private
     * @returns {boolean}
     */
    flush() {
        this.forEachPartition(partition => partition.flush());

        this.emit('flush');
        return true;
    }

    /**
     * Add an index entry for the given document at the position and size.
     *
     * @private
     * @param {number} partitionId The partition where the document is stored.
     * @param {number} position The file offset where the document is stored.
     * @param {number} size The size of the stored document.
     * @param {Object} document The document to add to the index.
     * @param {function} [callback] The callback to call when the index is written to disk.
     * @returns {Index.Entry} The index entry item.
     */
    addIndex(partitionId, position, size, document, callback) {

        /*if (this.index.lastEntry.position + this.index.lastEntry.size !== position) {
         this.emit('index-corrupted');
         throw new Error('Corrupted index, needs to be rebuilt!');
         }*/

        let entry = new Index.Entry(this.index.length + 1, position, size, partitionId);
        this.index.add(entry, (indexPosition) => {
            this.emit('wrote', document, entry, indexPosition);
            if (typeof callback === 'function') return callback(indexPosition);
        });
        return entry;
    }

    /**
     * Get a partition either by name or by id.
     * If a partition with the given name does not exist, a new one will be created.
     * If a partition with the given id does not exist, an error is thrown.
     *
     * @private
     * @param {string|number} partitionIdentifier The partition name or the partition Id
     * @returns {Partition}
     * @throws {Error} If an id is given and no such partition exists.
     */
    getPartition(partitionIdentifier) {
        if (typeof partitionIdentifier === 'string') {
            let partitionName = this.storageFile + (partitionIdentifier.length ? '.' + partitionIdentifier : '');
            partitionIdentifier = Partition.id(partitionName);
            if (!this.partitions[partitionIdentifier]) {
                this.partitions[partitionIdentifier] = new Partition(partitionName, this.partitionConfig);
            }
        } else if (!this.partitions[partitionIdentifier]) {
            throw new Error('Partition #' + partitionIdentifier + ' does not exist.');
        }

        this.partitions[partitionIdentifier].open();
        return this.partitions[partitionIdentifier];
    }

    /**
     * @api
     * @param {Object} document The document to write to storage.
     * @param {function} [callback] A function that will be called when the document is written to disk.
     * @returns {Number} The 1-based document sequence number in the storage.
     */
    write(document, callback) {
        let data = this.serializer.serialize(document).toString();
        let dataSize = Buffer.byteLength(data, 'utf8');

        let partitionName = this.partitioner(document, this.index.length + 1);
        let partition = this.getPartition(partitionName);
        let position = partition.write(data, callback);

        if (position === false) {
            throw new Error('Error writing document.');
        }
        let indexEntry = this.addIndex(partition.id, position, dataSize, document);
        this.forEachSecondaryIndex(index => index.add(indexEntry), document);

        return this.index.length;
    }

    /**
     * @private
     * @param {number} partitionId The partition to read from.
     * @param {number} position The file position to read from.
     * @param {number} [size] The expected byte size of the document at the given position.
     * @returns {Object} The document stored at the given position.
     * @throws {Error} if the document at the given position can not be deserialized.
     */
    readFrom(partitionId, position, size) {
        let partition = this.getPartition(partitionId);
        let data;
        try {
            data = partition.readFrom(position);
            return this.serializer.deserialize(data);
        } catch (e) {
            console.log('Error parsing document:', data, position);
            throw e;
        }
    }

    /**
     * Read a single document from the given position, in the full index or in the provided index.
     *
     * @api
     * @param {number} number The 1-based document number (inside the given index) to read.
     * @param {Index} [index] The index to use for finding the document position.
     * @returns {Object} The document at the given position inside the index.
     */
    read(number, index) {
        index = index || this.index;

        let entry = index.get(number);
        if (entry === false) return false;

        return this.readFrom(entry.partition, entry.position, entry.size);
    }

    /**
     * Read a range of documents from the given position range, in the full index or in the provided index.
     * Returns a generator in order to reduce memory usage and be able to read lots of documents with little latency.
     *
     * @api
     * @param {number} from The 1-based document number (inclusive) to start reading from.
     * @param {number} [until] The 1-based document number (inclusive) to read until. Defaults to index.length.
     * @param {Index} [index] The index to use for finding the documents in the range.
     * @returns {Generator} A generator that will read each document in the range one by one.
     */
    *readRange(from, until, index) {
        index = index || this.index;

        let entries = index.range(from, until);
        if (entries === false) {
            throw new Error('Range scan error.');
        }
        for (let entry of entries) {
            let document = this.readFrom(entry.partition, entry.position, entry.size);
            yield document;
        }
    }

    /**
     * @private
     * @param {Object} document The document to check against the matcher.
     * @param {Object|function} matcher An object of properties and their values that need to match in the object.
     * @returns {boolean} True if the document matches the matcher or false otherwise.
     */
    matches(document, matcher) {
        if (typeof matcher === 'undefined') return true;
        if (typeof document === 'undefined') return false;
        if (typeof matcher === 'function') return matcher(document);
        for (let prop of Object.getOwnPropertyNames(matcher)) {
            if (typeof matcher[prop] === 'object') {
                if (!this.matches(document[prop], matcher[prop])) {
                    return false;
                }
            } else {
                if (document[prop] !== matcher[prop]) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Open an existing index.
     *
     * @api
     * @param {string} name The index name.
     * @param {Object|function} [matcher] The matcher object or function that the index needs to have been defined with. If not given it will not be validated.
     * @returns {Index}
     */
    openIndex(name, matcher) {
        if (name in this.secondaryIndexes) {
            return this.secondaryIndexes[name].index;
        }

        let indexName = this.storageFile + '.' + name + '.index';
        let metadata;
        if (matcher) {
            metadata = { metadata: { matcher: typeof matcher === 'object' ? JSON.stringify(matcher) : matcher.toString() } };
        }

        let index = new Index(this.EntryClass, indexName, Object.assign({}, this.indexOptions, metadata));
        if (typeof index.metadata.matcher === 'object') {
            matcher = index.metadata.matcher;
        } else {
            matcher = eval('(' + index.metadata.matcher + ')');
        }
        this.secondaryIndexes[name] = { index, matcher };
        index.open();
        return index;
    }

    /**
     * Ensure that an index with the given name and document matcher exists.
     * Will create the index if it doesn't exist, otherwise return the existing index.
     *
     * @api
     * @param {string} name The index name.
     * @param {Object|function} [matcher] An object that describes the document properties that need to match to add it this index or a function that receives a document and returns true if the document should be indexed.
     * @returns {Index} The index containing all documents that match the query.
     */
    ensureIndex(name, matcher) {
        /*if (!this.isopen) {
            throw new Error('Storage is not open yet.');
        }*/
        if (name in this.secondaryIndexes) {
            return this.secondaryIndexes[name].index;
        }

        let indexName = this.storageFile + '.' + name + '.index';
        if (fs.existsSync(path.join(this.indexDirectory, indexName))) {
            return this.openIndex(name, matcher);
        }

        if (!matcher) {
            throw new Error('Need to specify a matcher.');
        }

        let metadata = { metadata: { matcher: typeof matcher === 'object' ? JSON.stringify(matcher) : matcher.toString() } };
        let newIndex = new Index(this.EntryClass, indexName, Object.assign({}, this.indexOptions, metadata));
        this.forEachDocument((document, indexEntry) => {
            if (this.matches(document, matcher)) {
                newIndex.add(indexEntry);
            }
        });

        this.secondaryIndexes[name] = { index: newIndex, matcher };
        this.emit('index-created', name);
        return newIndex;
    }

    /**
     * Truncate the storage after the given sequence number.
     *
     * @param {number} after The document sequence number to truncate after.
     */
    truncate(after) {
        /*
         To truncate the store following steps need to be done:

         1) find all partition positions after which their files should be truncated
         2) truncate all partitions accordingly
         3) truncate/rewrite all indexes
         */
        let entries = this.index.range(after + 1);  // We need the first entry that is cut off
        if (entries === false || entries.length === 0) {
            return;
        }

        if (after === 0) {
            this.forEachPartition(partition => partition.truncate(0));
        } else {
            let partitions = [];
            let numPartitions = Object.keys(this.partitions).length;
            for (let entry of entries) {
                if (partitions.indexOf(entry.partition) >= 0) continue;
                partitions.push(entry.partition);
                this.getPartition(entry.partition).truncate(entry.position);

                if (partitions.length === numPartitions) {
                    break;
                }
            }
        }

        this.index.truncate(after);
        this.forEachSecondaryIndex(index => index.truncate(index.find(after)));
    }

    /**
     * Helper method to iterate over all documents.
     *
     * @private
     * @param {function(Object, Index.Entry)} iterationHandler
     */
    forEachDocument(iterationHandler) {
        if (typeof iterationHandler !== 'function') return;

        let entries = this.index.all();
        if (entries === false) {
            return;
        }

        for (let entry of entries) {
            let document = this.readFrom(entry.partition, entry.position, entry.size);
            iterationHandler(document, entry);
        }
    }

    /**
     * Helper method to iterate over all secondary indexes.
     *
     * @private
     * @param {function(Index)} iterationHandler
     * @param {Object} [matchDocument] If supplied, only indexes the document matches on will be iterated.
     */
    forEachSecondaryIndex(iterationHandler, matchDocument) {
        if (typeof iterationHandler !== 'function') return;

        for (let indexName of Object.keys(this.secondaryIndexes)) {
            if (!matchDocument || this.matches(matchDocument, this.secondaryIndexes[indexName].matcher)) {
                iterationHandler(this.secondaryIndexes[indexName].index);
            }
        }
    }

    /**
     * Helper method to iterate over all partitions.
     *
     * @private
     * @param {function(Partition)} iterationHandler
     */
    forEachPartition(iterationHandler) {
        if (typeof iterationHandler !== 'function') return;

        for (let partition of Object.keys(this.partitions)) {
            iterationHandler(this.partitions[partition]);
        }
    }

}

module.exports = Storage;<commit_msg>Emit an event for every addition of a document to a secondary index<commit_after>const fs = require('fs');
const mkdirpSync = require('mkdirp').sync;
const path = require('path');
const EventEmitter = require('events');
const Partition = require('./Partition');
const Index = require('./Index');

const DEFAULT_READ_BUFFER_SIZE = 4 * 1024;
const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;

/**
 * An append-only storage with highly performant positional range scans.
 * It's highly optimized for an event-store and hence does not support compaction or data-rewrite, nor any querying
 */
class Storage extends EventEmitter {

    /**
     * Config options:
     *   - serializer: A serializer object with methods serialize(document) and deserialize(data). Default is JSON.stringify/parse.
     *   - dataDirectory: The path where the storage data should reside. Default '.'.
     *   - indexDirectory: The path where the indexes should be stored. Defaults to dataDirectory.
     *   - indexFile: The name of the primary index. Default '{storageName}.index'.
     *   - readBufferSize: Size of the read buffer in bytes. Default 4096.
     *   - writeBufferSize: Size of the write buffer in bytes. Default 16384.
     *   - maxWriteBufferDocuments: How many documents to have in the write buffer at max. 0 means as much as possible. Default 0.
     *   - syncOnFlush: If fsync should be called on write buffer flush. Set this if you need strict durability. Defaults to false.
     *   - partitioner: A function that takes a document and sequence number and returns a partition name that the document should be stored in. Defaults to write all documents to the primary partition.
     *   - indexOptions: An options object that should be passed to all indexes on construction.
     *
     * @param {string} [storageName] The name of the storage.
     * @param {Object} [config] An object with storage parameters.
     */
    constructor(storageName = 'storage', config = {}) {
        super();
        if (typeof storageName === 'object') {
            config = storageName;
            storageName = undefined;
        }

        this.storageFile = storageName || 'storage';
        let defaults = {
            serializer: { serialize: JSON.stringify, deserialize: JSON.parse },
            partitioner: (document, number) => '',
            dataDirectory: '.',
            indexFile: this.storageFile + '.index',
            indexOptions: {},
        };
        config = Object.assign(defaults, config);
        this.serializer = config.serializer;
        this.partitioner = config.partitioner;

        this.dataDirectory = path.resolve(config.dataDirectory);
        if (!fs.existsSync(this.dataDirectory)) {
            mkdirpSync(this.dataDirectory);
        }

        this.indexDirectory = path.resolve(config.indexDirectory || this.dataDirectory);

        this.indexOptions = config.indexOptions;
        this.indexOptions.dataDirectory = this.indexDirectory;
        this.index = new Index(config.indexFile, this.indexOptions);
        this.secondaryIndexes = {};

        this.scanPartitions(config);
    }

    /**
     * The amount of documents in the storage.
     * @returns {number}
     */
    get length() {
        return this.index.length;
    }

    /**
     * Scan the data directory for all existing partitions.
     * Every file beginning with the storageFile name is considered a partition.
     *
     * @private
     * @param {Object} config The configuration object containing options for the partitions.
     * @returns void
     */
    scanPartitions(config) {
        let defaults = {
            readBufferSize: DEFAULT_READ_BUFFER_SIZE,
            writeBufferSize: DEFAULT_WRITE_BUFFER_SIZE,
            maxWriteBufferDocuments: 0,
            syncOnFlush: false
        };
        this.partitionConfig = Object.assign(defaults, config);
        this.partitions = {};

        let files = fs.readdirSync(this.dataDirectory);
        for (let file of files) {
            if (file.substr(-6) === '.index') continue;
            if (file.substr(-7) === '.branch') continue;
            if (file.substr(0, this.storageFile.length) === this.storageFile) {
                //console.log('Found existing partition', file);
                let partition = new Partition(file, this.partitionConfig);
                this.partitions[partition.id] = partition;
            }
        }
    }

    /**
     * Open the storage and indexes and create read and write buffers.
     * Will emit an 'opened' event if finished.
     *
     * @api
     * @returns {boolean}
     */
    open() {
        this.index.open();

        // TODO: Open secondary indexes lazily
        this.forEachSecondaryIndex(index => index.open());

        this.emit('opened');
        return true;
    }

    /**
     * Close the storage and frees up all resources.
     * Will emit a 'closed' event when finished.
     *
     * @api
     * @returns void
     */
    close() {
        this.index.close();
        this.forEachSecondaryIndex(index => index.close());
        this.forEachPartition(partition => partition.close());
        this.emit('closed');
    }

    /**
     * Flush the write buffer to disk.
     * This is a sync method and will invoke all previously registered flush callbacks.
     * Will emit a 'flush' event when done.
     *
     * @private
     * @returns {boolean}
     */
    flush() {
        this.forEachPartition(partition => partition.flush());

        this.emit('flush');
        return true;
    }

    /**
     * Add an index entry for the given document at the position and size.
     *
     * @private
     * @param {number} partitionId The partition where the document is stored.
     * @param {number} position The file offset where the document is stored.
     * @param {number} size The size of the stored document.
     * @param {Object} document The document to add to the index.
     * @param {function} [callback] The callback to call when the index is written to disk.
     * @returns {Index.Entry} The index entry item.
     */
    addIndex(partitionId, position, size, document, callback) {

        /*if (this.index.lastEntry.position + this.index.lastEntry.size !== position) {
         this.emit('index-corrupted');
         throw new Error('Corrupted index, needs to be rebuilt!');
         }*/

        let entry = new Index.Entry(this.index.length + 1, position, size, partitionId);
        this.index.add(entry, (indexPosition) => {
            this.emit('wrote', document, entry, indexPosition);
            if (typeof callback === 'function') return callback(indexPosition);
        });
        return entry;
    }

    /**
     * Get a partition either by name or by id.
     * If a partition with the given name does not exist, a new one will be created.
     * If a partition with the given id does not exist, an error is thrown.
     *
     * @private
     * @param {string|number} partitionIdentifier The partition name or the partition Id
     * @returns {Partition}
     * @throws {Error} If an id is given and no such partition exists.
     */
    getPartition(partitionIdentifier) {
        if (typeof partitionIdentifier === 'string') {
            let partitionName = this.storageFile + (partitionIdentifier.length ? '.' + partitionIdentifier : '');
            partitionIdentifier = Partition.id(partitionName);
            if (!this.partitions[partitionIdentifier]) {
                this.partitions[partitionIdentifier] = new Partition(partitionName, this.partitionConfig);
            }
        } else if (!this.partitions[partitionIdentifier]) {
            throw new Error('Partition #' + partitionIdentifier + ' does not exist.');
        }

        this.partitions[partitionIdentifier].open();
        return this.partitions[partitionIdentifier];
    }

    /**
     * @api
     * @param {Object} document The document to write to storage.
     * @param {function} [callback] A function that will be called when the document is written to disk.
     * @returns {Number} The 1-based document sequence number in the storage.
     */
    write(document, callback) {
        let data = this.serializer.serialize(document).toString();
        let dataSize = Buffer.byteLength(data, 'utf8');

        let partitionName = this.partitioner(document, this.index.length + 1);
        let partition = this.getPartition(partitionName);
        let position = partition.write(data, callback);

        if (position === false) {
            throw new Error('Error writing document.');
        }
        let indexEntry = this.addIndex(partition.id, position, dataSize, document);
        this.forEachSecondaryIndex(index => {
            index.add(indexEntry);
            this.emit('index-add', index.name, index.length, document);
        }, document);

        return this.index.length;
    }

    /**
     * @private
     * @param {number} partitionId The partition to read from.
     * @param {number} position The file position to read from.
     * @param {number} [size] The expected byte size of the document at the given position.
     * @returns {Object} The document stored at the given position.
     * @throws {Error} if the document at the given position can not be deserialized.
     */
    readFrom(partitionId, position, size) {
        let partition = this.getPartition(partitionId);
        let data;
        try {
            data = partition.readFrom(position);
            return this.serializer.deserialize(data);
        } catch (e) {
            console.log('Error parsing document:', data, position);
            throw e;
        }
    }

    /**
     * Read a single document from the given position, in the full index or in the provided index.
     *
     * @api
     * @param {number} number The 1-based document number (inside the given index) to read.
     * @param {Index} [index] The index to use for finding the document position.
     * @returns {Object} The document at the given position inside the index.
     */
    read(number, index) {
        index = index || this.index;

        let entry = index.get(number);
        if (entry === false) return false;

        return this.readFrom(entry.partition, entry.position, entry.size);
    }

    /**
     * Read a range of documents from the given position range, in the full index or in the provided index.
     * Returns a generator in order to reduce memory usage and be able to read lots of documents with little latency.
     *
     * @api
     * @param {number} from The 1-based document number (inclusive) to start reading from.
     * @param {number} [until] The 1-based document number (inclusive) to read until. Defaults to index.length.
     * @param {Index} [index] The index to use for finding the documents in the range.
     * @returns {Generator} A generator that will read each document in the range one by one.
     */
    *readRange(from, until, index) {
        index = index || this.index;

        let entries = index.range(from, until);
        if (entries === false) {
            throw new Error('Range scan error.');
        }
        for (let entry of entries) {
            let document = this.readFrom(entry.partition, entry.position, entry.size);
            yield document;
        }
    }

    /**
     * @private
     * @param {Object} document The document to check against the matcher.
     * @param {Object|function} matcher An object of properties and their values that need to match in the object.
     * @returns {boolean} True if the document matches the matcher or false otherwise.
     */
    matches(document, matcher) {
        if (typeof matcher === 'undefined') return true;
        if (typeof document === 'undefined') return false;
        if (typeof matcher === 'function') return matcher(document);
        for (let prop of Object.getOwnPropertyNames(matcher)) {
            if (typeof matcher[prop] === 'object') {
                if (!this.matches(document[prop], matcher[prop])) {
                    return false;
                }
            } else {
                if (document[prop] !== matcher[prop]) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Open an existing index.
     *
     * @api
     * @param {string} name The index name.
     * @param {Object|function} [matcher] The matcher object or function that the index needs to have been defined with. If not given it will not be validated.
     * @returns {Index}
     * @throws {Error} if the index with that name does not exist.
     */
    openIndex(name, matcher) {
        if (name in this.secondaryIndexes) {
            return this.secondaryIndexes[name].index;
        }

        let indexName = this.storageFile + '.' + name + '.index';
        if (!fs.existsSync(path.join(this.indexDirectory, indexName))) {
            throw new Error('Index "' + name + '" does not exist.');
        }
        let metadata;
        if (matcher) {
            metadata = { metadata: { matcher: typeof matcher === 'object' ? JSON.stringify(matcher) : matcher.toString() } };
        }

        let index = new Index(this.EntryClass, indexName, Object.assign({}, this.indexOptions, metadata));
        if (typeof index.metadata.matcher === 'object') {
            matcher = index.metadata.matcher;
        } else {
            matcher = eval('(' + index.metadata.matcher + ')');
        }
        this.secondaryIndexes[name] = { index, matcher };
        index.open();
        return index;
    }

    /**
     * Ensure that an index with the given name and document matcher exists.
     * Will create the index if it doesn't exist, otherwise return the existing index.
     *
     * @api
     * @param {string} name The index name.
     * @param {Object|function} [matcher] An object that describes the document properties that need to match to add it this index or a function that receives a document and returns true if the document should be indexed.
     * @returns {Index} The index containing all documents that match the query.
     * @throws {Error} if the index doesn't exist yet and no matcher was specified.
     */
    ensureIndex(name, matcher) {
        /*if (!this.isopen) {
            throw new Error('Storage is not open yet.');
        }*/
        if (name in this.secondaryIndexes) {
            return this.secondaryIndexes[name].index;
        }

        let indexName = this.storageFile + '.' + name + '.index';
        if (fs.existsSync(path.join(this.indexDirectory, indexName))) {
            return this.openIndex(name, matcher);
        }

        if (!matcher) {
            throw new Error('Need to specify a matcher.');
        }

        let metadata = { metadata: { matcher: typeof matcher === 'object' ? JSON.stringify(matcher) : matcher.toString() } };
        let newIndex = new Index(this.EntryClass, indexName, Object.assign({}, this.indexOptions, metadata));
        this.forEachDocument((document, indexEntry) => {
            if (this.matches(document, matcher)) {
                newIndex.add(indexEntry);
            }
        });

        this.secondaryIndexes[name] = { index: newIndex, matcher };
        this.emit('index-created', name);
        return newIndex;
    }

    /**
     * Truncate the storage after the given sequence number.
     *
     * @param {number} after The document sequence number to truncate after.
     */
    truncate(after) {
        /*
         To truncate the store following steps need to be done:

         1) find all partition positions after which their files should be truncated
         2) truncate all partitions accordingly
         3) truncate/rewrite all indexes
         */
        let entries = this.index.range(after + 1);  // We need the first entry that is cut off
        if (entries === false || entries.length === 0) {
            return;
        }

        if (after === 0) {
            this.forEachPartition(partition => partition.truncate(0));
        } else {
            let partitions = [];
            let numPartitions = Object.keys(this.partitions).length;
            for (let entry of entries) {
                if (partitions.indexOf(entry.partition) >= 0) continue;
                partitions.push(entry.partition);
                this.getPartition(entry.partition).truncate(entry.position);

                if (partitions.length === numPartitions) {
                    break;
                }
            }
        }

        this.index.truncate(after);
        this.forEachSecondaryIndex(index => index.truncate(index.find(after)));
    }

    /**
     * Helper method to iterate over all documents.
     *
     * @private
     * @param {function(Object, Index.Entry)} iterationHandler
     */
    forEachDocument(iterationHandler) {
        if (typeof iterationHandler !== 'function') return;

        let entries = this.index.all();
        if (entries === false) {
            return;
        }

        for (let entry of entries) {
            let document = this.readFrom(entry.partition, entry.position, entry.size);
            iterationHandler(document, entry);
        }
    }

    /**
     * Helper method to iterate over all secondary indexes.
     *
     * @private
     * @param {function(Index)} iterationHandler
     * @param {Object} [matchDocument] If supplied, only indexes the document matches on will be iterated.
     */
    forEachSecondaryIndex(iterationHandler, matchDocument) {
        if (typeof iterationHandler !== 'function') return;

        for (let indexName of Object.keys(this.secondaryIndexes)) {
            if (!matchDocument || this.matches(matchDocument, this.secondaryIndexes[indexName].matcher)) {
                iterationHandler(this.secondaryIndexes[indexName].index);
            }
        }
    }

    /**
     * Helper method to iterate over all partitions.
     *
     * @private
     * @param {function(Partition)} iterationHandler
     */
    forEachPartition(iterationHandler) {
        if (typeof iterationHandler !== 'function') return;

        for (let partition of Object.keys(this.partitions)) {
            iterationHandler(this.partitions[partition]);
        }
    }

}

module.exports = Storage;<|endoftext|>


<commit_before>
Tributary.plugin("screenshot", tributaryScreenshotPlugin);

function tributaryScreenshotPlugin(tributary, plugin) {
  var el;
  plugin.activate = function()  {
    //console.log("hi from screenshot plugin");
    el = document.getElementById(plugin.elId);
    // get the screenshot button and put it on the config panel
    d3.select("#thumbnail-content").node().appendChild(d3.select(el).select("#screenshot").node());
    d3.select("#screenshot").on("click", handleScreenshot);
    var link = tributary.__config__.get("thumbnail");
    // Update the thumbnail in the config panel
    if (link) {
      d3.select("#thumbnail-content").select("img").attr("src", link).style("display", "");
    }
  }
  plugin.deactivate = function()  {
    el = document.getElementById(plugin.elId);
    el.innerHTML = "";
  }

  tributary._screenshot = handleScreenshot;
  function handleScreenshot() {
    var display = tributary.__config__.get("display");
    if(display === "svg") {
      svgScreenshot();
    } else if(display === "canvas" || display === "webgl") {
      canvasScreenshot();
    } else if(display === "div") {
      htmlScreenshot();
    }
  }
  function svgScreenshot() {
    //svg = d3.select("#display").html();
    var svg = new XMLSerializer().serializeToString(d3.select(".tributary_svg").node());
    var findstring = '<svg xmlns="http://www.w3.org/2000/svg" xlink:xlink="http://www.w3.org/1999/xlink" class="tributary_svg" width="100%" height="100%">';
    svg = svg.replace(findstring,'<svg>'); // hacky.
    //console.log("SVG", d3.select("#display").html())
    canvas = document.getElementById('pngit');
    canvg(canvas, svg, {renderCallback: function() {
      var len = "data:image/png;base64,".length;
      var img = canvas.toDataURL("image/png").substring(len);
      tributary.events.trigger("imgur", img);
    }});
  }
  function canvasScreenshot() {
    canvas = d3.select("#display canvas").node()
    var len = "data:image/png;base64,".length;
    var img = canvas.toDataURL("image/png").substring(len);
    tributary.events.trigger("imgur", img);
  }
  function htmlScreenshot() {
    html2canvas( [ d3.select("#display").node() ], {
      onrendered: function(canvas) {
        var len = "data:image/png;base64,".length;
        var img = canvas.toDataURL("image/png").substring(len);
        tributary.events.trigger("imgur", img);
    }
    });
  }

  return plugin;
}
<commit_msg>fix bug with certain svg setups<commit_after>
Tributary.plugin("screenshot", tributaryScreenshotPlugin);

function tributaryScreenshotPlugin(tributary, plugin) {
  var el;
  plugin.activate = function()  {
    //console.log("hi from screenshot plugin");
    el = document.getElementById(plugin.elId);
    // get the screenshot button and put it on the config panel
    d3.select("#thumbnail-content").node().appendChild(d3.select(el).select("#screenshot").node());
    d3.select("#screenshot").on("click", handleScreenshot);
    var link = tributary.__config__.get("thumbnail");
    // Update the thumbnail in the config panel
    if (link) {
      d3.select("#thumbnail-content").select("img").attr("src", link).style("display", "");
    }
  }
  plugin.deactivate = function()  {
    el = document.getElementById(plugin.elId);
    el.innerHTML = "";
  }

  tributary._screenshot = handleScreenshot;
  function handleScreenshot() {
    var display = tributary.__config__.get("display");
    if(display === "svg") {
      svgScreenshot();
    } else if(display === "canvas" || display === "webgl") {
      canvasScreenshot();
    } else if(display === "div") {
      htmlScreenshot();
    }
  }
  function svgScreenshot() {
    //svg = d3.select("#display").html();
    var svg = new XMLSerializer().serializeToString(d3.select(".tributary_svg").node());
    //var findstring = '<svg xmlns="http://www.w3.org/2000/svg" xlink:xlink="http://www.w3.org/1999/xlink" class="tributary_svg" width="100%" height="100%">';
    //svg = svg.replace(findstring,''); // hacky.
    svg = svg.replace(/^\<svg xmlns.+tributary_svg.+?>/,''); // hacky.

    canvas = document.getElementById('pngit');
    canvg(canvas, svg, {renderCallback: function() {
      var len = "data:image/png;base64,".length;
      var img = canvas.toDataURL("image/png").substring(len);
      tributary.events.trigger("imgur", img);
    }});
  }
  function canvasScreenshot() {
    canvas = d3.select("#display canvas").node()
    var len = "data:image/png;base64,".length;
    var img = canvas.toDataURL("image/png").substring(len);
    tributary.events.trigger("imgur", img);
  }
  function htmlScreenshot() {
    html2canvas( [ d3.select("#display").node() ], {
      onrendered: function(canvas) {
        var len = "data:image/png;base64,".length;
        var img = canvas.toDataURL("image/png").substring(len);
        tributary.events.trigger("imgur", img);
    }
    });
  }

  return plugin;
}
<|endoftext|>


<commit_before>/**
 * Текстуры используются для отрисовки изображений в WebGL
 */
class Texture {
    /**
     * @param {HTMLImageElement | HTMLCanvasElement} [src=null] В качестве изображения может быть
     * либо элемент img, либо canvas
     */
    constructor(src) {
        this._src = src || null;

        /**
         * Тип фильтра при отображении текстуры, размеры которой больше, чем размеры исходной картинки
         * @type {TextureFilter}
         */
        this.magFilter = Texture.LinearFilter;

        /**
         * Тип фильтра при отображении текстуры, размеры которой меньше, чем размеры исходной картинки
         * @type {TextureFilter}
         */
        this.minFilter = Texture.LinearMipMapLinearFilter;

        /**
         * Что делать, если ширина исходной картинки не равна степени 2.
         * @type {TextureClamp}
         */
        this.wrapS = Texture.ClampToEdgeWrapping;

        /**
         * Что делать, если высота исходной картинки не равна степени 2.
         * @type {TextureClamp}
         */
        this.wrapT = Texture.ClampToEdgeWrapping;

        /**
         * Формат текстуры (RGBA или Alpha)
         * @type {TextureFormat}
         */
        this.format = Texture.RgbaFormat;

        /**
         * Генерировать ли mipmaps.
         * Они значительно повышают качество и производительность отображения.
         * Mipmaps могут использоваться только, если размеры текстуры равны степени 2.
         *
         * @type {Boolean}
         */
        this.generateMipmaps = true;

        /**
         * Нужно ли отражать текстуру относительно оси Y.
         * @type {Boolean}
         */
        this.flipY = true;

        /**
         * Сохранен ли цвет текстуры с premultiply аlpha.
         * @type {Boolean}
         */
        this.premultiplyAlpha = true;

        /**
         * Контекст WebGL, в котором была инициализирована текстура.
         * Используется только для удаления, подумать хорошо, прежде чем использовать для чего-то ещё.
         * @type {?WebGLRenderingContext}
         * @ignore
         */
        this._glContext = null;
    }

    /**
     * Связывает WebGL и данные текстуры.
     * При первом вызов происходит инициализация.
     *
     * @param {WebGLRenderingContext} gl
     * @param {?Number} index Номер текстуры в контексте WebGL.
     * Если его нет, используется уже активированный юнит текстуры.
     */
    enable(gl, index) {
        if (index !== undefined) {
            gl.activeTexture(gl.TEXTURE0 + index);
        }

        if (!this._texture) {
            this._prepare(gl);
        }

        gl.bindTexture(gl.TEXTURE_2D, this._texture);

        return this;
    }

    /**
     * Удаляет текстуру из видеокарты
     */
    remove() {
        if (this._texture) {
            this._glContext.deleteTexture(this._texture);
            this._glContext = null;
            this._texture = null;
        }

        return this;
    }

    _prepare(gl) {
        this._glContext = gl;
        this._texture = gl.createTexture();

        gl.bindTexture(gl.TEXTURE_2D, this._texture);

        if (this.flipY) {
            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        }

        if (this.premultiplyAlpha) {
            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
        }

        if (this.size) {
            gl.texImage2D(
                gl.TEXTURE_2D,
                0,
                this._toGlParam(gl, this.format),
                this.size[0],
                this.size[1],
                0,
                this._toGlParam(gl, this.format),
                gl.UNSIGNED_BYTE,
                this._src
            );
        } else {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._src);
        }

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this._toGlParam(gl, this.wrapS));
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this._toGlParam(gl, this.wrapT));

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._toGlParam(gl, this.magFilter));
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._toGlParam(gl, this.minFilter));

        if (this.generateMipmaps &&
            this.minFilter !== Texture.NearestFilter &&
            this.minFilter !== Texture.LinearFilter
        ) {
            gl.generateMipmap(gl.TEXTURE_2D);
        }

        gl.bindTexture(gl.TEXTURE_2D, null);
    }

    _toGlParam(gl, param) {
        if (param === Texture.ClampToEdgeWrapping) { return gl.CLAMP_TO_EDGE; }
        if (param === Texture.Repeat) { return gl.REPEAT; }
        if (param === Texture.MirroredRepeat) { return gl.MIRRORED_REPEAT; }

        if (param === Texture.NearestFilter) { return gl.NEAREST; }
        if (param === Texture.NearestMipMapNearestFilter) { return gl.NEAREST_MIPMAP_NEAREST; }
        if (param === Texture.NearestMipMapLinearFilter) { return gl.NEAREST_MIPMAP_LINEAR; }

        if (param === Texture.LinearFilter) { return gl.LINEAR; }
        if (param === Texture.LinearMipMapNearestFilter) { return gl.LINEAR_MIPMAP_NEAREST; }
        if (param === Texture.LinearMipMapLinearFilter) { return gl.LINEAR_MIPMAP_LINEAR; }
        if (param === Texture.RgbaFormat) { return gl.RGBA; }
        if (param === Texture.AlphaFormat) { return gl.ALPHA; }
        return null;
    }
}

Texture.ClampToEdgeWrapping = 8;
Texture.Repeat = 9;
Texture.MirroredRepeat = 10;

Texture.NearestFilter = 1;
Texture.NearestMipMapNearestFilter = 2;
Texture.NearestMipMapLinearFilter = 3;
Texture.LinearFilter = 4;
Texture.LinearMipMapNearestFilter = 5;
Texture.LinearMipMapLinearFilter = 6;

Texture.RgbaFormat = 11;
Texture.AlphaFormat = 12;

export default Texture;

/**
 * @typedef {Texture.NearestFilter | Texture.NearestMipMapNearestFilter |
 * Texture.NearestMipMapLinearFilter | Texture.LinearFilter |
 * Texture.LinearMipMapNearestFilter | Texture.LinearMipMapLinearFilter} TextureFilter
 */

/**
 * @typedef {Texture.ClampToEdgeWrapping} TextureClamp
 */

/**
 * @typedef {Texture.RgbaFormat | Texture.AlphaFormat} TextureFormat
 */
<commit_msg>Fix texture default parameters (#88)<commit_after>/**
 * Текстуры используются для отрисовки изображений в WebGL
 */
class Texture {
    /**
     * @param {HTMLImageElement | HTMLCanvasElement} [src=null] В качестве изображения может быть
     * либо элемент img, либо canvas
     */
    constructor(src) {
        this._src = src || null;

        /**
         * Тип фильтра при отображении текстуры, размеры которой больше, чем размеры исходной картинки
         * @type {TextureFilter}
         */
        this.magFilter = Texture.LinearFilter;

        /**
         * Тип фильтра при отображении текстуры, размеры которой меньше, чем размеры исходной картинки
         * @type {TextureFilter}
         */
        this.minFilter = Texture.LinearMipMapLinearFilter;

        /**
         * Что делать, если ширина исходной картинки не равна степени 2.
         * @type {TextureClamp}
         */
        this.wrapS = Texture.ClampToEdgeWrapping;

        /**
         * Что делать, если высота исходной картинки не равна степени 2.
         * @type {TextureClamp}
         */
        this.wrapT = Texture.ClampToEdgeWrapping;

        /**
         * Формат текстуры (RGBA или Alpha)
         * @type {TextureFormat}
         */
        this.format = Texture.RgbaFormat;

        /**
         * Генерировать ли mipmaps.
         * Они значительно повышают качество и производительность отображения.
         * Mipmaps могут использоваться только, если размеры текстуры равны степени 2.
         *
         * @type {Boolean}
         */
        this.generateMipmaps = true;

        /**
         * Нужно ли отражать текстуру относительно оси Y.
         * @type {Boolean}
         */
        this.flipY = true;

        /**
         * Сохранен ли цвет текстуры с premultiply аlpha.
         * @type {Boolean}
         */
        this.premultiplyAlpha = true;

        /**
         * Контекст WebGL, в котором была инициализирована текстура.
         * Используется только для удаления, подумать хорошо, прежде чем использовать для чего-то ещё.
         * @type {?WebGLRenderingContext}
         * @ignore
         */
        this._glContext = null;
    }

    /**
     * Связывает WebGL и данные текстуры.
     * При первом вызов происходит инициализация.
     *
     * @param {WebGLRenderingContext} gl
     * @param {?Number} index Номер текстуры в контексте WebGL.
     * Если его нет, используется уже активированный юнит текстуры.
     */
    enable(gl, index) {
        if (index !== undefined) {
            gl.activeTexture(gl.TEXTURE0 + index);
        }

        if (!this._texture) {
            this._prepare(gl);
        }

        gl.bindTexture(gl.TEXTURE_2D, this._texture);

        return this;
    }

    /**
     * Удаляет текстуру из видеокарты
     */
    remove() {
        if (this._texture) {
            this._glContext.deleteTexture(this._texture);
            this._glContext = null;
            this._texture = null;
        }

        return this;
    }

    _prepare(gl) {
        this._glContext = gl;
        this._texture = gl.createTexture();

        gl.bindTexture(gl.TEXTURE_2D, this._texture);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY);
        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);

        if (this.size) {
            gl.texImage2D(
                gl.TEXTURE_2D,
                0,
                this._toGlParam(gl, this.format),
                this.size[0],
                this.size[1],
                0,
                this._toGlParam(gl, this.format),
                gl.UNSIGNED_BYTE,
                this._src
            );
        } else {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._src);
        }

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this._toGlParam(gl, this.wrapS));
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this._toGlParam(gl, this.wrapT));

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this._toGlParam(gl, this.magFilter));
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this._toGlParam(gl, this.minFilter));

        if (this.generateMipmaps &&
            this.minFilter !== Texture.NearestFilter &&
            this.minFilter !== Texture.LinearFilter
        ) {
            gl.generateMipmap(gl.TEXTURE_2D);
        }

        gl.bindTexture(gl.TEXTURE_2D, null);
    }

    _toGlParam(gl, param) {
        if (param === Texture.ClampToEdgeWrapping) { return gl.CLAMP_TO_EDGE; }
        if (param === Texture.Repeat) { return gl.REPEAT; }
        if (param === Texture.MirroredRepeat) { return gl.MIRRORED_REPEAT; }

        if (param === Texture.NearestFilter) { return gl.NEAREST; }
        if (param === Texture.NearestMipMapNearestFilter) { return gl.NEAREST_MIPMAP_NEAREST; }
        if (param === Texture.NearestMipMapLinearFilter) { return gl.NEAREST_MIPMAP_LINEAR; }

        if (param === Texture.LinearFilter) { return gl.LINEAR; }
        if (param === Texture.LinearMipMapNearestFilter) { return gl.LINEAR_MIPMAP_NEAREST; }
        if (param === Texture.LinearMipMapLinearFilter) { return gl.LINEAR_MIPMAP_LINEAR; }
        if (param === Texture.RgbaFormat) { return gl.RGBA; }
        if (param === Texture.AlphaFormat) { return gl.ALPHA; }
        return null;
    }
}

Texture.ClampToEdgeWrapping = 8;
Texture.Repeat = 9;
Texture.MirroredRepeat = 10;

Texture.NearestFilter = 1;
Texture.NearestMipMapNearestFilter = 2;
Texture.NearestMipMapLinearFilter = 3;
Texture.LinearFilter = 4;
Texture.LinearMipMapNearestFilter = 5;
Texture.LinearMipMapLinearFilter = 6;

Texture.RgbaFormat = 11;
Texture.AlphaFormat = 12;

export default Texture;

/**
 * @typedef {Texture.NearestFilter | Texture.NearestMipMapNearestFilter |
 * Texture.NearestMipMapLinearFilter | Texture.LinearFilter |
 * Texture.LinearMipMapNearestFilter | Texture.LinearMipMapLinearFilter} TextureFilter
 */

/**
 * @typedef {Texture.ClampToEdgeWrapping} TextureClamp
 */

/**
 * @typedef {Texture.RgbaFormat | Texture.AlphaFormat} TextureFormat
 */
<|endoftext|>


<commit_before>

// Change check info

const checkInfo = [
  {
    "check": "checkOne",
    "title": "The amount does not coincide with the average amount",
    "description": "
Each merchant usually sells in a specific range of products. Fraudsters want to get the most out of their transactions and therefore have amounts that are much higher than the average. However, keep in mind that what is a “normal” region for amounts to be in can differ between merchants.
"
  },
  {
    "check": "checkTwo",
    "title": "Shopper email or card number is used in quick succession",
    "description": "
Velocity checks allow merchants to set velocity thresholds on various customer attributes, controlling how often a customer can attempt transactions. These checks are intended to identify high-speed fraud attacks. To best utilize these checks merchants need to understand the behavior of their shoppers. The average number of transactions by a good user varies significantly across merchants.

When the same email address or card number is used often within a small period, this is a good case for fraud.
"
  },
  {
    "check": "checkThree",
    "title": "Shopper country is high risk",
    "description": "
Some countries pose a high risk for fraud, regardless of what the country of the currency or issuing country is, such as Mexico or Bulgaria.
"
  },
  {
    "check": "checkFour",
    "title": "Different countries used by the same shopper email address",
    "description": "
Fraudsters often have a fraud profile that spans multiple regions. When one shopper email address or card number gets associated with multiple countries, this is an indication of possible fraud.
"
  },
  {
    "check": "checkFive",
    "title": "Shopper country differs from issuing country and/or country of currency",
    "description": "
This risk check is triggered when a transaction has the shopper country different from the issuing country (of the card) or that is different from the country from which the currency comes.
"
  },
  {
    "check": "checkSix",
    "title": "Card number already used by other shopper (shopper email)",
    "description": "
Fraudsters often create multiple accounts and attempt to use the same compromised account with different techniques and attack merchants. This check is aimed at identifying when a card number is being used across multiple accounts.

Note that there are some legitimate cases in which this would occur:

The user may have multiple accounts.

It may be a shared card in a family or business setting

"
  },
  {
    "check": "checkSeven",
    "title": "Transaction time check",
    "description": "
Most merchants notice that fraudsters tend to visit their site during certain parts of the day. It is not uncommon, for example, for fraud to spike during the night hours while legitimate transactions are limited.
"
  }
];

const $trigger = $('.triggerCheck');
const checkTitle = document.getElementsByClassName('header-check');
const checkDescription = document.getElementsByClassName('description-check');
let CURRENTLY_SELECTED_CHECK = checkInfo[0].check; // barchart.js looks at this to see what check is active
                                                   // terrible code, too tired to care

function drawBarChart(element, data, threshold) {

  var svg = d3.select(element),
    margin = {top: 20, right: 20, bottom: 110, left: 40},
    margin2 = {top: 430, right: 20, bottom: 30, left: 40},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    height2 = +svg.attr("height") - margin2.top - margin2.bottom;

  var x = d3.scaleLinear().range([0, width]),
    x2 = d3.scaleLinear().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    y2 = d3.scaleLinear().range([height2, 0]);

  var xAxis = d3.axisBottom(x),
    xAxis2 = d3.axisBottom(x2),
    yAxis = d3.axisLeft(y);

  var brush = d3.brushX()
    .extent([[0, 0], [width, height2]])
    .on("brush end", brushed);

  var area = d3.area()
    .curve(d3.curveMonotoneX)
    .x(function(d, i) { return x(i); })
    .y0(height)
    .y1(function(d) { return y(d.total); });

  var area2 = d3.area()
    .curve(d3.curveMonotoneX)
    .x(function(d, i) { return x2(i); })
    .y0(height2)
    .y1(function(d) { return y2(d.total); });

  svg.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

  var focus = svg.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var context = svg.append("g")
    .attr("class", "context")
    .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

    x.domain(d3.extent(data, function(d, i) { return i; }));
    y.domain([0, d3.max(data, function(d) { return d.total; })]);
    x2.domain(x.domain());
    y2.domain(y.domain());

  function drawBars(dataset, selection) {
    var bars = focus.selectAll(".bar").data(dataset, datum => datum);
    var segment = width / dataset.length;
    var tenth = (segment / 10);
    var barW = (segment / 10) * 8;

    bars.exit().remove();

    //ENTER
    bars.enter()
          .append("rect")
        // .attr("class", "bar")
        .attr("class", function(d) { return d.total < FRAUD_THRESHOLD + 1 ? "bar" : "bar is-fraud" })
        .attr("x", function(d, i) { return x( i + selection[0] ) - (barW / 2); })
        .attr("y", function(d) { return y(d.total); })
        .attr("width", function() { return barW; })
        .attr("height", function(d) { return height - y(d.total); });

    bars.enter()
          .append("rect")
        .attr("class", "bar enter")
        .attr("x", function(d, i) { return x(i + selection[0]) - (barW / 2); })
        .attr("y", function(d) { return y(d[CURRENTLY_SELECTED_CHECK]); }) // CURRENTLY_SELECTED_CHECK is defined in triggercheck.js
        .attr("width", function() { return barW; })
        .attr("height", function(d) { return height - y(d[CURRENTLY_SELECTED_CHECK]); });

    focus.append('line')
         .attr("x1", 0)
         .attr("y1", y(threshold))
         .attr("x2", width)
         .attr("y2", y(threshold))
         .attr("stroke-width", 2)
         .attr("stroke", "#cc333f");
  }

  function triggerCheck() {
    $trigger.removeClass('is-active');
    $(this).addClass('is-active');
    const selectedCheck = $(this).attr('data-for').substring(6, 7) - 1;
    checkTitle[0].innerHTML = checkInfo[selectedCheck].title;
    checkDescription[0].innerHTML = checkInfo[selectedCheck].description;
    CURRENTLY_SELECTED_CHECK = checkInfo[selectedCheck].check;
    // drawBarChart('.horizontalbarchart', TOTAL_DATA.sort((left, right) => right.total - left.total), FRAUD_THRESHOLD);
    drawBars(data, x2.range());
  }

  drawBars(data, x2.range());

  focus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

  focus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis);

  context.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area2);

  context.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height2 + ")")
    .call(xAxis2);

  context.append("g")
    .attr("class", "brush")
    .call(brush)
    .call(brush.move, [0, x.range()[1] / 3]);

  function brushed() {
    var selectedRange = d3.event.selection;
    var selectedRangeWidth = Math.abs(d3.event.selection[0] - d3.event.selection[1]);
    var minSelectionWidth = 15;
    if (selectedRangeWidth < minSelectionWidth) {
      selectedRange = [selectedRange[0], selectedRange[0] + minSelectionWidth];
    }
    var s = selectedRange || x2.range();
    var xs = s.map(x2.invert, x2);

    x.domain(xs);

    // from Programmatically Control a d3 brush
    // http://bl.ocks.org/timelyportfolio/5c136de85de1c2abb6fc
    /*if (!d3.event.sourceEvent) return; // Only transition after input.
    if (!d3.event.selection) return; // Ignore empty selections.
    d3.select(this).transition().call(d3.event.target.move, selectedRange);
*/

    focus.select(".area").attr("d", area);
    focus.select(".axis--x").call(xAxis);

    drawBars(data.slice(xs[0], xs[1]), xs);

  }

  $trigger.click(triggerCheck);

}

$(function() {
  checkTitle[0].innerHTML = checkInfo[0].title;
  checkDescription[0].innerHTML = checkInfo[0].description;
});
<commit_msg>Barchart interaction FIXED woohoo<commit_after>// Change check info

const checkInfo = [
  {
    "check": "checkOne",
    "title": "The amount does not coincide with the average amount",
    "description": "
Each merchant usually sells in a specific range of products. Fraudsters want to get the most out of their transactions and therefore have amounts that are much higher than the average. However, keep in mind that what is a “normal” region for amounts to be in can differ between merchants.
"
  },
  {
    "check": "checkTwo",
    "title": "Shopper email or card number is used in quick succession",
    "description": "
Velocity checks allow merchants to set velocity thresholds on various customer attributes, controlling how often a customer can attempt transactions. These checks are intended to identify high-speed fraud attacks. To best utilize these checks merchants need to understand the behavior of their shoppers. The average number of transactions by a good user varies significantly across merchants.

When the same email address or card number is used often within a small period, this is a good case for fraud.
"
  },
  {
    "check": "checkThree",
    "title": "Shopper country is high risk",
    "description": "
Some countries pose a high risk for fraud, regardless of what the country of the currency or issuing country is, such as Mexico or Bulgaria.
"
  },
  {
    "check": "checkFour",
    "title": "Different countries used by the same shopper email address",
    "description": "
Fraudsters often have a fraud profile that spans multiple regions. When one shopper email address or card number gets associated with multiple countries, this is an indication of possible fraud.
"
  },
  {
    "check": "checkFive",
    "title": "Shopper country differs from issuing country and/or country of currency",
    "description": "
This risk check is triggered when a transaction has the shopper country different from the issuing country (of the card) or that is different from the country from which the currency comes.
"
  },
  {
    "check": "checkSix",
    "title": "Card number already used by other shopper (shopper email)",
    "description": "
Fraudsters often create multiple accounts and attempt to use the same compromised account with different techniques and attack merchants. This check is aimed at identifying when a card number is being used across multiple accounts.

Note that there are some legitimate cases in which this would occur:

The user may have multiple accounts.

It may be a shared card in a family or business setting

"
  },
  {
    "check": "checkSeven",
    "title": "Transaction time check",
    "description": "
Most merchants notice that fraudsters tend to visit their site during certain parts of the day. It is not uncommon, for example, for fraud to spike during the night hours while legitimate transactions are limited.
"
  }
];

const $trigger = $('.triggerCheck');
const checkTitle = document.getElementsByClassName('header-check');
const checkDescription = document.getElementsByClassName('description-check');
let CURRENTLY_SELECTED_CHECK = checkInfo[0].check; // barchart.js looks at this to see what check is active
                                                   // terrible code, too tired to care
function drawBarChart(element, data, threshold) {

  var svg = d3.select(element),
    margin = {top: 20, right: 20, bottom: 110, left: 40},
    margin2 = {top: 430, right: 20, bottom: 30, left: 40},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    height2 = +svg.attr("height") - margin2.top - margin2.bottom;

  var x = d3.scaleLinear().range([0, width]),
    x2 = d3.scaleLinear().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    y2 = d3.scaleLinear().range([height2, 0]);

  var xAxis = d3.axisBottom(x),
    xAxis2 = d3.axisBottom(x2),
    yAxis = d3.axisLeft(y);

  var brush = d3.brushX()
    .extent([[0, 0], [width, height2]])
    .on("brush end", brushed);

  var area = d3.area()
    .curve(d3.curveMonotoneX)
    .x(function(d, i) { return x(i); })
    .y0(height)
    .y1(function(d) { return y(d.total); });

  var area2 = d3.area()
    .curve(d3.curveMonotoneX)
    .x(function(d, i) { return x2(i); })
    .y0(height2)
    .y1(function(d) { return y2(d.total); });

  svg.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

  var focus = svg.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var context = svg.append("g")
    .attr("class", "context")
    .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

    x.domain(d3.extent(data, function(d, i) { return i; }));
    y.domain([0, d3.max(data, function(d) { return d.total; })]);
    x2.domain(x.domain());
    y2.domain(y.domain());

  function drawBars(dataset, selection) {
    var bars = focus.selectAll(".bar").data(dataset, datum => datum);
    var segment = width / dataset.length;
    var tenth = (segment / 10);
    var barW = (segment / 10) * 8;

    bars.exit().remove();

    //ENTER
    bars.enter()
          .append("rect")
        // .attr("class", "bar")
        .attr("class", function(d) { return d.total < FRAUD_THRESHOLD + 1 ? "bar" : "bar is-fraud" })
        .attr("x", function(d, i) { return x( i + selection[0] ) - (barW / 2); })
        .attr("y", function(d) { return y(d.total); })
        .attr("width", function() { return barW; })
        .attr("height", function(d) { return height - y(d.total); });

    bars.enter()
          .append("rect")
        .attr("class", "bar enter")
        .attr("x", function(d, i) { return x(i + selection[0]) - (barW / 2); })
        .attr("y", function(d) { return y(d[CURRENTLY_SELECTED_CHECK]); }) // CURRENTLY_SELECTED_CHECK is defined in triggercheck.js
        .attr("width", function() { return barW; })
        .attr("height", function(d) { return height - y(d[CURRENTLY_SELECTED_CHECK]); });

    focus.append('line')
         .attr("x1", 0)
         .attr("y1", y(threshold))
         .attr("x2", width)
         .attr("y2", y(threshold))
         .attr("stroke-width", 2)
         .attr("stroke", "#cc333f");
  }

  var s = x2.range();

  function triggerCheck() {
    $trigger.removeClass('is-active');
    $(this).addClass('is-active');
    const selectedCheck = $(this).attr('data-for').substring(6, 7) - 1;
    checkTitle[0].innerHTML = checkInfo[selectedCheck].title;
    checkDescription[0].innerHTML = checkInfo[selectedCheck].description;
    CURRENTLY_SELECTED_CHECK = checkInfo[selectedCheck].check;
    drawBars(data, x2.range());
    d3.select('.brush').call(brush.move, [0, s[1]]);
  }

  drawBars(data, x2.range());

  focus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

  focus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis);

  context.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area2);

  context.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height2 + ")")
    .call(xAxis2);

  context.append("g")
    .attr("class", "brush")
    .call(brush)
    .call(brush.move, [0, x.range()[1] / 3]);

  function brushed() {
    var selectedRange = d3.event.selection;
    var selectedRangeWidth = Math.abs(d3.event.selection[0] - d3.event.selection[1]);
    var minSelectionWidth = 15;
    if (selectedRangeWidth < minSelectionWidth) {
      selectedRange = [selectedRange[0], selectedRange[0] + minSelectionWidth];
    }
    s = selectedRange || x2.range();
    var xs = s.map(x2.invert, x2);

    x.domain(xs);

    // from Programmatically Control a d3 brush
    // http://bl.ocks.org/timelyportfolio/5c136de85de1c2abb6fc
    /*if (!d3.event.sourceEvent) return; // Only transition after input.
    if (!d3.event.selection) return; // Ignore empty selections.
    d3.select(this).transition().call(d3.event.target.move, selectedRange);
*/

    focus.select(".area").attr("d", area);
    focus.select(".axis--x").call(xAxis);

    drawBars(data.slice(xs[0], xs[1]), xs);

  }

  $trigger.click(triggerCheck);

}

$(function() {
  checkTitle[0].innerHTML = checkInfo[0].title;
  checkDescription[0].innerHTML = checkInfo[0].description;
});
<|endoftext|>


<commit_before>/**
 * ConfirmationModal interactor
 */

import { interactor, scoped, computed } from '@bigtest/interactor';
import ButtonInteractor from '../../Button/tests/interactor';

const tagName = selector => computed(function () {
  return this.$(selector).tagName.toLowerCase();
});

export default interactor(class ModalFooterInteractor {
  confirmButton = scoped('[data-test-confirmation-modal-confirm-button]', ButtonInteractor);
  cancelButton = scoped('[data-test-confirmation-modal-cancel-button]', ButtonInteractor);
  bodyTagName = tagName('[data-test-confirmation-modal-message]');
});
<commit_msg>STCOM-710: Extend `ConfirmationModal` interactor (#1318)<commit_after>/**
 * ConfirmationModal interactor
 */

import {
  interactor,
  scoped,
  computed,
  Interactor,
} from '@bigtest/interactor';
import ButtonInteractor from '../../Button/tests/interactor';

import css from '../../Modal/Modal.css';

const tagName = selector => computed(function () {
  return this.$(selector).tagName.toLowerCase();
});

export default interactor(class ConfirmationModalInteractor {
  heading = new Interactor(`.${css.modalLabel}`);
  body = new Interactor('[data-test-confirmation-modal-message]');
  bodyTagName = tagName('[data-test-confirmation-modal-message]');
  confirmButton = scoped('[data-test-confirmation-modal-confirm-button]', ButtonInteractor);
  cancelButton = scoped('[data-test-confirmation-modal-cancel-button]', ButtonInteractor);
});
<|endoftext|>


<commit_before>/// Wrappers for chrome...sendMessage

var M = module.exports = {};

var util = require('./util');

function convertMessage(msg) {
    if (typeof msg !== 'object') {
        return {name: msg};
    }
    return msg;
}

function convertSender(sender) {
    if (!sender) {
        return {};
    }
    if (typeof sender !== 'object') {
        return sender;
    }
    var k = Object.keys(sender);
    if (k.length === 1 && k[0] === 'id') {
        return 'background';
    }
    if (sender.tab) {
        var p = Object.assign({}, sender);
        p.tabId = sender.tab.id;
        return p;
    }
    return sender;
}

function toBackground(msg) {
    msg.to = 'background';
    return new Promise(function (resolve) {
        chrome.runtime.sendMessage(msg, function (res) {
            resolve({receiver: 'background', data: res});
        });
    });
}

function toFrame(msg, frame) {
    msg.to = frame;
    return new Promise(function (resolve) {
        chrome.tabs.sendMessage(frame.tabId, msg, {frameId: frame.frameId}, function (res) {
            resolve({receiver: frame, data: res});
        });
    });
}

function toFrameList(msg, frames) {
    return Promise.all(frames.map(function (frame) {
        return toFrame(msg, frame);
    }));
}

M.enumFrames = function (tabFilter) {

    function framesInTab(tab) {
        return new Promise(function (resolve) {
            chrome.webNavigation.getAllFrames({tabId: tab.id}, function (frames) {

                if (!frames) {
                    // Vivaldi, as of 1.8.770.56, doesn't support getAllFrames() properly
                    // let's pretend there's only one top frame
                    frames = [{
                        errorOccurred: false,
                        frameId: 0,
                        parentFrameId: -1
                    }];
                }

                resolve(frames.map(function (f) {
                    f.tabId = tab.id;
                    return f;
                }));
            });
        });
    }

    if (tabFilter === 'active') {
        tabFilter = {active: true, currentWindow: true};
    }

    return new Promise(function (resolve) {
        chrome.tabs.query(tabFilter || {}, function (tabs) {
            Promise.all(tabs.map(framesInTab)).then(function (res) {
                resolve(util.flatten(res));
            });
        });
    });
};

M.background = function (msg) {
    console.log('MESSAGE: background', msg);
    return toBackground(convertMessage(msg));
};

M.frame = function (msg, frame) {
    console.log('MESSAGE: frame', msg, frame);
    return toFrame(convertMessage(msg), frame);
};

M.allFrames = function (msg) {
    console.log('MESSAGE: allFrames', msg);
    msg = convertMessage(msg);
    return M.enumFrames('active').then(function (frames) {
        return toFrameList(msg, frames);
    });
};

M.topFrame = function (msg) {
    console.log('MESSAGE: topFrame', msg);
    msg = convertMessage(msg);
    return M.enumFrames('active').then(function (frames) {
        var top = frames.filter(function (f) {
            return f.frameId === 0;
        });
        if (top.length) {
            return toFrame(msg, top[0]);
        }
    });
};

M.broadcast = function (msg) {
    console.log('MESSAGE: broadcast', msg);
    msg = convertMessage(msg);
    return M.enumFrames().then(function (frames) {
        return toFrameList(msg, frames);
    });
};

M.listen = function (listeners) {
    chrome.runtime.onMessage.addListener(function (msg, sender, fn) {
        if (listeners[msg.name]) {
            msg.sender = convertSender(sender);
            var res = listeners[msg.name](msg);
            //console.log('RESPONDING TO', msg, res);
            return fn(res);
        }
        console.log('LOST', msg.name);
    });
};
<commit_msg>sendMessage checks<commit_after>/// Wrappers for chrome...sendMessage

var M = module.exports = {};

var util = require('./util');

function convertMessage(msg) {
    if (typeof msg !== 'object') {
        return {name: msg};
    }
    return msg;
}

function convertSender(sender) {
    if (!sender) {
        return {};
    }
    if (typeof sender !== 'object') {
        return sender;
    }
    var k = Object.keys(sender);
    if (k.length === 1 && k[0] === 'id') {
        return 'background';
    }
    if (sender.tab) {
        var p = Object.assign({}, sender);
        p.tabId = sender.tab.id;
        return p;
    }
    return sender;
}

function toBackground(msg) {
    msg.to = 'background';
    return new Promise(function (resolve) {
        try {
            chrome.runtime.sendMessage(msg, function (res) {
                resolve({receiver: 'background', data: res});
            });
        } catch (e) {
            console.log('FAILED toBackground', e);
            resolve({receiver: 'background', data: null})
        }
    });
}

function toFrame(msg, frame) {
    msg.to = frame;
    return new Promise(function (resolve) {
        try {
            chrome.tabs.sendMessage(frame.tabId, msg, {frameId: frame.frameId}, function (res) {
                resolve({receiver: frame, data: res});
            });
        } catch (e) {
            console.log('FAILED toFrame', e);
            resolve({receiver: null, data: null})
        }
    });
}

function toFrameList(msg, frames) {
    return Promise.all(frames.map(function (frame) {
        return toFrame(msg, frame);
    }));
}

M.enumFrames = function (tabFilter) {

    function framesInTab(tab) {
        return new Promise(function (resolve) {
            chrome.webNavigation.getAllFrames({tabId: tab.id}, function (frames) {

                if (!frames) {
                    // Vivaldi, as of 1.8.770.56, doesn't support getAllFrames() properly
                    // let's pretend there's only one top frame
                    frames = [{
                        errorOccurred: false,
                        frameId: 0,
                        parentFrameId: -1
                    }];
                }

                resolve(frames.map(function (f) {
                    f.tabId = tab.id;
                    return f;
                }));
            });
        });
    }

    if (tabFilter === 'active') {
        tabFilter = {active: true, currentWindow: true};
    }

    return new Promise(function (resolve) {
        chrome.tabs.query(tabFilter || {}, function (tabs) {
            Promise.all(tabs.map(framesInTab)).then(function (res) {
                resolve(util.flatten(res));
            });
        });
    });
};

M.background = function (msg) {
    console.log('MESSAGE: background', msg);
    return toBackground(convertMessage(msg));
};

M.frame = function (msg, frame) {
    console.log('MESSAGE: frame', msg, frame);
    return toFrame(convertMessage(msg), frame);
};

M.allFrames = function (msg) {
    console.log('MESSAGE: allFrames', msg);
    msg = convertMessage(msg);
    return M.enumFrames('active').then(function (frames) {
        return toFrameList(msg, frames);
    });
};

M.topFrame = function (msg) {
    console.log('MESSAGE: topFrame', msg);
    msg = convertMessage(msg);
    return M.enumFrames('active').then(function (frames) {
        var top = frames.filter(function (f) {
            return f.frameId === 0;
        });
        if (top.length) {
            return toFrame(msg, top[0]);
        }
    });
};

M.broadcast = function (msg) {
    console.log('MESSAGE: broadcast', msg);
    msg = convertMessage(msg);
    return M.enumFrames().then(function (frames) {
        return toFrameList(msg, frames);
    });
};

M.listen = function (listeners) {
    chrome.runtime.onMessage.addListener(function (msg, sender, fn) {
        if (listeners[msg.name]) {
            msg.sender = convertSender(sender);
            var res = listeners[msg.name](msg);
            //console.log('RESPONDING TO', msg, res);
            return fn(res);
        }
        console.log('LOST', msg.name);
    });
};
<|endoftext|>