import { createGrid, drawD3, remove } from './DrawSpec'; import {filter} from './Filter'; import {fit} from './Optimizer'; import {processActorsFirst, processEventsFirst} from './PreProcessing'; import {Config, Data, RenderedPoint, BaseConfig, FullConfig} from './Types'; function uuid(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } // Fallback for environments without crypto.randomUUID (e.g. older jsdom) return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } export default class Storygram { // Data with filtering and optimization public processedData!: Data; // Data without filtering and optimization public data!: Data; // Array containing a grid of rendered points, the x length and the maximal y length private renderedGrid!: [RenderedPoint[], number, number]; // Whether the storygram has been filtered, optimized and rendered private isCalculated: boolean = false; // Default values public baseConfig: BaseConfig = { uid: uuid(), verbose: false, colorScheme: 'schemeAccent', lineSize: 9, eventDescription: l => String(l.eventValue), url: (event, actor) => 'https://www.google.ch/search?q=' + String(event.eventValue) + ' ' + actor.actorID, eventUrl: (event) => 'https://www.google.ch/search?q=' + String(event.eventValue), marginTop: 50, marginBottom: 50, marginLeft: 50, urlOpensNewTab: true, marginRight: 50, eventPadding: 40, actorPadding: 30, eventValueScaling: 0.9, continuous: false, compact: false, highlight: [], strokeWidth: (event, actor) => 0, actorColor: (event, actor) => actor.actorID, mustContain: [], shouldContain: [], interactedWith: [[], 0], filterEventValue: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], filterGroupSize: [0, Number.MAX_SAFE_INTEGER], filterEventCustom: () => true, filterActorCustom: () => true, filterEventValueLifeTime: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], filterGroupAmt: [0, Number.MAX_SAFE_INTEGER], linearLoss: 1, amtLoss: 1, lengthLoss: 1, yExtentLoss: 0, root: 'body', tooltipPosition: 'absolute', hiddenActorsTooltipTitle: 'Hidden actors', inferredEventType: undefined }; // Custom and default configuration public config!: FullConfig; public constructor(rawData: any[], config: Config) { this.setConfig(config); this.setData(rawData); this.calculate(); } public setConfig(config: Config) { this.config = {...this.baseConfig, ...config}; this.isCalculated = false } public setData(data: any[]): void { if (Array.isArray(data)) { switch (this.config.dataFormat) { case 'ranges': this.data = processActorsFirst(data, this.config); break; case 'table': const splitFuncTable = this.config.actorSplitFunction ? this.config.actorSplitFunction : (arg: string | string[]) => typeof arg === 'string' ? [arg] : arg this.data = processEventsFirst(data, this.config.actorFields, splitFuncTable, this.config, this.config.eventField); break; case 'array': const splitFuncArray = (arg: string[]) => { return arg.reduce((arr, a) => this.config.actorSplitFunction ? arr.concat(this.config.actorSplitFunction(a)) : arr.concat(a), new Array()) } this.data = processEventsFirst(data, [this.config.actorArrayField], splitFuncArray, this.config, this.config.eventField); break; default: console.error('Incompatible data format. Specify a data format of type ranges, table or array.'); } } else { console.error('Incompatible data format. Provide data in array shape.'); } this.isCalculated = false } // Imports the data and applies filtering public calculate() { this.processedData = filter(this.data, this.config); this.isCalculated = true } // Draw the storygram on the DOM. If the importation and filtering steps aren't yet made, perform these first public draw() { if(!this.isCalculated) { this.calculate() } this.processedData = fit(this.processedData, this.config); this.renderedGrid = createGrid(this.processedData, this.config); if (this.config.verbose) { console.log(this.renderedGrid); } this.remove() if(this.processedData.events.length !== 0 && this.processedData.actors.size !== 0) { drawD3(this.renderedGrid, this.config) } else { console.warn('Storygram: No data after filtering') } } public remove() { remove(this.config) } }