UNPKG

@cull/pore

Version:

An email data analysis library.

98 lines (92 loc) 3 kB
import SankeyGraph, { SankeyNode } from './sankey'; import Data from './data'; /** * Datum collection analyzing the origins of email. */ export default class Origin extends Data { /** * Distinct sender values * e.g. `["jon <jon@mail.example.com>",...]` */ senders!: Set<string>; /** * Distinct addresses * e.g. `["jon@mail.example.com",...]` */ addresses!: Set<string>; /** * Distinct domains * e.g. `["example.com",...]` */ domains!: Set<string>; /** * Compute properties dependent on filtered data. */ protected compute(): void { super.compute(); /** * Though seemingly uncommon in practice, technically there may be more than one "from" header. * Currently, treat each value as a distinct message. A natural side effect of this approach is that * counting distinct "from" values may be greater than the total number of distinct Datum. * * @todo Is there a better way to handle the scenario where more than one "from" value is present? * @todo Should the "from" values be filtered to guarantee uniqueness in the context of Datum? */ this.senders = new Set(); this.addresses = new Set(); this.domains = new Set(); this.data .flatMap(datum => datum.from) .forEach(identity => { this.senders.add(identity.toString()); this.addresses.add(identity.address); this.domains.add(identity.domain); }); } /** * Generate Sankey graph nodes and links for email origins against filtered data. * * @todo benchmark * @todo seems like the kind of processing that could move to worker? */ sankey(): SankeyGraph { let graph = new SankeyGraph(); this.senders.forEach(sender => { let data = this.data.filter(d => d.from.find(f => f.toString() === sender)); if (data.length > 0) { let first = data[0].from.find(f => f.toString() === sender); if (first !== undefined) { let address = first.address; let source = createNode({ sender }); let target = createNode({ address }); graph.append({ source, target, data }); } } }); this.addresses.forEach(address => { let data = this.data.filter(d => d.from.find(f => f.address === address)); if (data.length > 0) { let first = data[0].from.find(f => f.address === address); if (first !== undefined) { let domain = first.domain; let source = createNode({ address }); let target = createNode({ domain }); graph.append({ source, target, data }); } } }); return graph; } } /** * Convenience utility to create a SankeyNode entity from a terse object as input. */ export let createNode = ( n: { sender: string } | { address: string } | { domain: string } ): SankeyNode => { let kind = Object.keys(n)[0]; let label = n[kind]; let id = `${kind}__${label}`; let node = { kind, label, id }; return node; };