UNPKG

@reactivex/rxjs

Version:

Reactive Extensions for modern JavaScript

364 lines 15.9 kB
/*eslint-disable no-param-reassign, no-use-before-define*/ var gm = require('gm'); var _ = require('lodash'); var Color = require('color'); var CANVAS_WIDTH = 1280; var canvasHeight; var CANVAS_PADDING = 20; var OBSERVABLE_HEIGHT = 200; var OPERATOR_HEIGHT = 140; var ARROW_HEAD_SIZE = 18; var DEFAULT_MAX_FRAME = 10; var OBSERVABLE_END_PADDING = 5 * ARROW_HEAD_SIZE; var MARBLE_RADIUS = 32; var COMPLETE_HEIGHT = MARBLE_RADIUS; var TALLER_COMPLETE_HEIGHT = 1.8 * MARBLE_RADIUS; var SIN_45 = 0.707106; var NESTED_STREAM_ANGLE = 18; // degrees var TO_RAD = (Math.PI / 180); var MESSAGES_WIDTH = (CANVAS_WIDTH - 2 * CANVAS_PADDING - OBSERVABLE_END_PADDING); var BLACK_COLOR = '#101010'; var COLORS = ['#3EA1CB', '#FFCB46', '#FF6946', '#82D736']; var SPECIAL_COLOR = '#1010F0'; var MESSAGE_OVERLAP_HEIGHT = TALLER_COMPLETE_HEIGHT; function colorToGhostColor(hex) { return Color(hex).mix(Color('white')).hexString(); } function getMaxFrame(allStreams) { var allStreamsLen = allStreams.length; var max = 0; for (var i = 0; i < allStreamsLen; i++) { var messagesLen = allStreams[i].messages.length; for (var j = 0; j < messagesLen; j++) { if (allStreams[i].messages[j].frame > max) { max = allStreams[i].messages[j].frame; } } } return max; } function stringToColor(str) { var smallPrime1 = 59; var smallPrime2 = 97; var hash = str.split('') .map(function (x) { return x.charCodeAt(0); }) .reduce(function (x, y) { return (x * smallPrime1) + (y * smallPrime2); }, 1); return COLORS[hash % COLORS.length]; } function isNestedStreamData(message) { return message.notification.kind === 'N' && message.notification.value && message.notification.value.messages; } function areEqualStreamData(leftStreamData, rightStreamData) { if (leftStreamData.messages.length !== rightStreamData.messages.length) { return false; } for (var i = 0; i < leftStreamData.messages.length; i++) { var left = leftStreamData.messages[i]; var right = rightStreamData.messages[i]; if (left.frame !== right.frame) { return false; } if (left.notification.kind !== right.notification.kind) { return false; } if (left.notification.value !== right.notification.value) { return false; } } return true; } function measureObservableArrow(maxFrame, streamData) { var startX = CANVAS_PADDING + MESSAGES_WIDTH * (streamData.subscription.start / maxFrame); var MAX_MESSAGES_WIDTH = CANVAS_WIDTH - CANVAS_PADDING; var lastMessageFrame = streamData.messages .reduce(function (acc, msg) { var frame = msg.frame; return frame > acc ? frame : acc; }, 0); var subscriptionEndX = CANVAS_PADDING + MESSAGES_WIDTH * (streamData.subscription.end / maxFrame) + OBSERVABLE_END_PADDING; var streamEndX = startX + MESSAGES_WIDTH * (lastMessageFrame / maxFrame) + OBSERVABLE_END_PADDING; var endX = (streamData.subscription.end === '100%') ? MAX_MESSAGES_WIDTH : Math.max(streamEndX, subscriptionEndX); return { startX: startX, endX: endX }; } function measureInclination(startX, endX, angle) { var length = endX - startX; var cotAngle = Math.cos(angle * TO_RAD) / Math.sin(angle * TO_RAD); return (length / cotAngle); } function measureNestedStreamHeight(maxFrame, streamData) { var measurements = measureObservableArrow(maxFrame, streamData); var startX = measurements.startX; var endX = measurements.endX; return measureInclination(startX, endX, NESTED_STREAM_ANGLE); } function amountPriorOverlaps(message, messageIndex, otherMessages) { return otherMessages.reduce(function (acc, otherMessage, otherIndex) { if (otherIndex < messageIndex && otherMessage.frame === message.frame && message.notification.kind === 'N' && otherMessage.notification.kind === 'N') { return acc + 1; } return acc; }, 0); } function measureStreamHeight(maxFrame) { return function measureStreamHeightWithMaxFrame(streamData) { var messages = streamData.messages; var maxMessageHeight = messages .map(function (msg, index) { var height = isNestedStreamData(msg) ? measureNestedStreamHeight(maxFrame, msg.notification.value) + OBSERVABLE_HEIGHT * 0.25 : OBSERVABLE_HEIGHT * 0.5; var overlapHeightBonus = amountPriorOverlaps(msg, index, messages) * MESSAGE_OVERLAP_HEIGHT; return height + overlapHeightBonus; }) .reduce(function (acc, curr) { return curr > acc ? curr : acc; }, 0); maxMessageHeight = Math.max(maxMessageHeight, OBSERVABLE_HEIGHT * 0.5); // to avoid zero return OBSERVABLE_HEIGHT * 0.5 + maxMessageHeight; }; } function drawObservableArrow(out, maxFrame, y, angle, streamData, isSpecial) { var measurements = measureObservableArrow(maxFrame, streamData); var startX = measurements.startX; var endX = measurements.endX; var outlineColor = BLACK_COLOR; if (isSpecial) { outlineColor = SPECIAL_COLOR; } if (streamData.isGhost) { outlineColor = colorToGhostColor(outlineColor); } out = out.stroke(outlineColor, 3); var inclination = measureInclination(startX, endX, angle); out = out.drawLine(startX, y, endX, y + inclination); out = out.draw('translate', String(endX) + ',' + String(y + inclination), 'rotate ' + String(angle), 'line', String(0) + ',' + String(0), String(-ARROW_HEAD_SIZE * 2) + ',' + String(-ARROW_HEAD_SIZE), 'line', String(0) + ',' + String(0), String(-ARROW_HEAD_SIZE * 2) + ',' + String(+ARROW_HEAD_SIZE)); return out; } function stringifyContent(content) { var string = content; if (Array.isArray(content)) { string = '[' + content.join(',') + ']'; } else if (typeof content === 'boolean') { return content ? 'true' : 'false'; } else if (typeof content === 'object') { string = JSON.stringify(content).replace(/"/g, ''); } return String('"' + string + '"'); } function drawMarble(out, x, y, inclination, content, isSpecial, isGhost) { var fillColor = stringToColor(stringifyContent(content)); var outlineColor = BLACK_COLOR; if (isSpecial) { outlineColor = SPECIAL_COLOR; } if (isGhost) { outlineColor = colorToGhostColor(outlineColor); fillColor = colorToGhostColor(fillColor); } out = out.stroke(outlineColor, 3); out = out.fill(fillColor); out = out.drawEllipse(x, y + inclination, MARBLE_RADIUS, MARBLE_RADIUS, 0, 360); out = out.strokeWidth(-1); out = out.fill(outlineColor); out = out.font('helvetica', 28); out = out.draw('translate ' + (x - CANVAS_WIDTH * 0.5) + ',' + (y + inclination - canvasHeight * 0.5), 'gravity Center', 'text 0,0', stringifyContent(content)); return out; } function drawError(out, x, y, startX, angle, isSpecial, isGhost) { var inclination = measureInclination(startX, x, angle); var outlineColor = BLACK_COLOR; if (isSpecial) { outlineColor = SPECIAL_COLOR; } if (isGhost) { outlineColor = colorToGhostColor(outlineColor); } out = out.stroke(outlineColor, 3); out = out.draw('translate', String(x) + ',' + String(y + inclination), 'rotate ' + String(angle), 'line', String(-MARBLE_RADIUS * SIN_45) + ',' + String(-MARBLE_RADIUS * SIN_45), String(+MARBLE_RADIUS * SIN_45) + ',' + String(+MARBLE_RADIUS * SIN_45), 'line', String(+MARBLE_RADIUS * SIN_45) + ',' + String(-MARBLE_RADIUS * SIN_45), String(-MARBLE_RADIUS * SIN_45) + ',' + String(+MARBLE_RADIUS * SIN_45)); return out; } function drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial, isGhost) { var startX = CANVAS_PADDING + MESSAGES_WIDTH * (streamData.subscription.start / maxFrame); var isOverlapping = streamData.messages.some(function (msg) { if (msg.notification.kind !== 'N') { return false; } var msgX = startX + MESSAGES_WIDTH * (msg.frame / maxFrame); return Math.abs(msgX - x) < MARBLE_RADIUS; }); var outlineColor = BLACK_COLOR; if (isSpecial) { outlineColor = SPECIAL_COLOR; } if (isGhost) { outlineColor = colorToGhostColor(outlineColor); } var inclination = measureInclination(startX, x, angle); var radius = isOverlapping ? TALLER_COMPLETE_HEIGHT : COMPLETE_HEIGHT; out = out.stroke(outlineColor, 3); out = out.draw('translate', String(x) + ',' + String(y + inclination), 'rotate ' + String(angle), 'line', String(0) + ',' + String(-radius), String(0) + ',' + String(+radius)); return out; } function drawNestedObservable(out, maxFrame, y, streamData) { var angle = NESTED_STREAM_ANGLE; out = drawObservableArrow(out, maxFrame, y, angle, streamData, false); out = drawObservableMessages(out, maxFrame, y, angle, streamData, false); return out; } function drawObservableMessages(out, maxFrame, baseY, angle, streamData, isSpecial) { var startX = CANVAS_PADDING + MESSAGES_WIDTH * (streamData.subscription.start / maxFrame); var messages = streamData.messages; messages.slice().reverse().forEach(function (message, reversedIndex) { if (message.frame < 0) { return; } var index = messages.length - reversedIndex - 1; var x = startX + MESSAGES_WIDTH * (message.frame / maxFrame); if (x - MARBLE_RADIUS < 0) { x += MARBLE_RADIUS; } var y = baseY + amountPriorOverlaps(message, index, messages) * MESSAGE_OVERLAP_HEIGHT; var inclination = measureInclination(startX, x, angle); switch (message.notification.kind) { case 'N': if (isNestedStreamData(message)) { out = drawNestedObservable(out, maxFrame, y, message.notification.value); } else { out = drawMarble(out, x, y, inclination, message.notification.value, isSpecial, streamData.isGhost); } break; case 'E': out = drawError(out, x, y, startX, angle, isSpecial, streamData.isGhost); break; case 'C': out = drawComplete(out, x, y, maxFrame, angle, streamData, isSpecial, streamData.isGhost); break; default: break; } }); return out; } function drawObservable(out, maxFrame, y, streamData, isSpecial) { var offsetY = OBSERVABLE_HEIGHT * 0.5; var angle = 0; out = drawObservableArrow(out, maxFrame, y + offsetY, angle, streamData, isSpecial); out = drawObservableMessages(out, maxFrame, y + offsetY, angle, streamData, isSpecial); return out; } function drawOperator(out, label, y) { out = out.stroke(BLACK_COLOR, 3); out = out.fill('#FFFFFF00'); out = out.drawRectangle(CANVAS_PADDING, y, CANVAS_WIDTH - CANVAS_PADDING, y + OPERATOR_HEIGHT); out = out.strokeWidth(-1); out = out.fill(BLACK_COLOR); out = out.font('helvetica', 54); out = out.draw('translate 0,' + (y + OPERATOR_HEIGHT * 0.5 - canvasHeight * 0.5), 'gravity Center', 'text 0,0', stringifyContent(label)); return out; } // Remove cold inputStreams which are already nested in some higher order stream function removeDuplicateInputs(inputStreams, outputStreams) { return inputStreams.filter(function (inputStream) { return !inputStreams.concat(outputStreams).some(function (otherStream) { return otherStream.messages.some(function (msg) { var passes = isNestedStreamData(msg) && inputStream.cold && _.isEqual(msg.notification.value.messages, inputStream.cold.messages); if (passes) { if (inputStream.cold.subscriptions.length) { msg.notification.value.subscription = { start: inputStream.cold.subscriptions[0].subscribedFrame, end: inputStream.cold.subscriptions[0].unsubscribedFrame }; } } return passes; }); }); }); } // For every inner stream in a higher order stream, create its ghost version // A ghost stream is a reference to an Observable that has not yet executed, // and is painted as a semi-transparent stream. function addGhostInnerInputs(inputStreams) { for (var i = 0; i < inputStreams.length; i++) { var inputStream = inputStreams[i]; for (var j = 0; j < inputStream.messages.length; j++) { var message = inputStream.messages[j]; if (isNestedStreamData(message) && typeof message.isGhost !== 'boolean') { var referenceTime = message.frame; if (!message.notification.value.subscription) { // There was no subscription at all, so this nested Observable is ghost message.isGhost = true; message.notification.value.isGhost = true; message.frame = referenceTime; message.notification.value.subscription = { start: referenceTime, end: 0 }; continue; } var subscriptionTime = message.notification.value.subscription.start; if (referenceTime !== subscriptionTime) { message.isGhost = false; message.notification.value.isGhost = false; message.frame = subscriptionTime; var ghost = _.cloneDeep(message); ghost.isGhost = true; ghost.notification.value.isGhost = true; ghost.frame = referenceTime; ghost.notification.value.subscription.start = referenceTime; ghost.notification.value.subscription.end -= subscriptionTime - referenceTime; inputStream.messages.push(ghost); } } } } return inputStreams; } function sanitizeHigherOrderInputStreams(inputStreams, outputStreams) { var newInputStreams = removeDuplicateInputs(inputStreams, outputStreams); newInputStreams = addGhostInnerInputs(newInputStreams); return newInputStreams; } module.exports = function painter(inputStreams, operatorLabel, outputStreams, filename) { inputStreams = sanitizeHigherOrderInputStreams(inputStreams, outputStreams); var maxFrame = getMaxFrame(inputStreams.concat(outputStreams)) || DEFAULT_MAX_FRAME; var allStreamsHeight = inputStreams.concat(outputStreams) .map(measureStreamHeight(maxFrame)) .reduce(function (x, y) { return x + y; }, 0); canvasHeight = allStreamsHeight + OPERATOR_HEIGHT; var heightSoFar = 0; var out; out = gm(CANVAS_WIDTH, canvasHeight, '#ffffff'); inputStreams.forEach(function (streamData) { out = drawObservable(out, maxFrame, heightSoFar, streamData, false); heightSoFar += measureStreamHeight(maxFrame)(streamData); }); out = drawOperator(out, operatorLabel, heightSoFar); heightSoFar += OPERATOR_HEIGHT; outputStreams.forEach(function (streamData) { var isSpecial = inputStreams.length > 0 && areEqualStreamData(inputStreams[0], streamData); out = drawObservable(out, maxFrame, heightSoFar, streamData, isSpecial); heightSoFar += measureStreamHeight(maxFrame)(streamData); }); out.write(filename, function (err) { if (err) { return console.error(arguments); } }); }; //# sourceMappingURL=painter.js.map