/**
* Activity Timeline Component
* Reusable, configuration-driven timeline visualization for time-based metadata
* Supports multiple modes: TSCM, Listening Post, Bluetooth, WiFi, Monitoring
*/
const ActivityTimeline = (function() {
'use strict';
// Default configuration
const defaults = {
// Identity
title: 'Activity Timeline',
mode: 'generic',
// Display options
visualMode: 'enriched', // 'compact' | 'enriched' | 'summary'
collapsed: true,
showAnnotations: true,
showLegend: true,
// Time configuration
timeWindows: {
'5m': 5 * 60 * 1000,
'15m': 15 * 60 * 1000,
'30m': 30 * 60 * 1000,
'1h': 60 * 60 * 1000,
'2h': 2 * 60 * 60 * 1000,
'4h': 4 * 60 * 60 * 1000,
'8h': 8 * 60 * 60 * 1000
},
defaultWindow: '30m',
availableWindows: ['5m', '15m', '30m', '1h', '2h'],
// Filter configuration
filters: {
hideBaseline: { enabled: true, label: 'Hide Known', default: false },
showOnlyNew: { enabled: true, label: 'New Only', default: false },
showOnlyBurst: { enabled: true, label: 'Bursts', default: false }
},
customFilters: [],
// Limits
maxItems: 100,
maxDisplayedLanes: 15,
burstThreshold: 5,
burstWindow: 60 * 1000,
updateInterval: 5000,
barMinWidth: 2,
// Callbacks
onItemClick: null,
onItemFlag: null,
onExport: null,
// Label generator (can be overridden)
labelGenerator: null
};
// Instances registry for multi-instance support
const instances = new Map();
/**
* Create a new timeline instance
*/
function create(containerId, options = {}) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`ActivityTimeline: Container '${containerId}' not found`);
return null;
}
// Merge options with defaults
const config = mergeConfig(defaults, options);
// Create instance state
const state = {
containerId: containerId,
config: config,
items: new Map(),
annotations: [],
filterState: initFilterState(config),
timeWindow: config.defaultWindow,
tooltip: null,
updateTimer: null,
element: null
};
// Store instance
instances.set(containerId, state);
// Build DOM
state.element = buildTimeline(container, state);
// Setup interactions
setupEventListeners(state);
// Create tooltip
createTooltip(state);
// Start update cycle
startUpdateTimer(state);
// Initial render
render(state);
// Return public API bound to this instance
return createPublicAPI(containerId);
}
/**
* Merge user config with defaults
*/
function mergeConfig(defaults, options) {
const config = { ...defaults };
for (const key of Object.keys(options)) {
if (typeof options[key] === 'object' && !Array.isArray(options[key]) && options[key] !== null) {
config[key] = { ...defaults[key], ...options[key] };
} else {
config[key] = options[key];
}
}
return config;
}
/**
* Initialize filter state from config
*/
function initFilterState(config) {
const state = {};
for (const [key, filter] of Object.entries(config.filters)) {
if (filter.enabled) {
state[key] = filter.default || false;
}
}
for (const filter of config.customFilters) {
state[filter.key] = filter.default || false;
}
return state;
}
/**
* Create item data structure
*/
function createItem(id, label, options = {}) {
return {
id: id,
label: label,
type: options.type || 'generic',
events: [],
firstSeen: null,
lastSeen: null,
status: 'new',
pattern: null,
flagged: false,
eventCount: 0,
tags: options.tags || [],
metadata: options.metadata || {}
};
}
/**
* Generate label for an item (can be overridden via config)
*/
function generateLabel(id, state) {
if (state.config.labelGenerator) {
return state.config.labelGenerator(id);
}
return categorizeById(id, state.config.mode);
}
/**
* Default categorization by mode
*/
function categorizeById(id, mode) {
// RF frequency categorization
if (mode === 'rf' || mode === 'tscm' || mode === 'listening-post') {
const f = parseFloat(id);
if (!isNaN(f)) {
if (f >= 2400 && f <= 2500) return 'Wi-Fi 2.4GHz';
if (f >= 5150 && f <= 5850) return 'Wi-Fi 5GHz';
if (f >= 433 && f <= 434) return '433MHz ISM';
if (f >= 868 && f <= 869) return '868MHz ISM';
if (f >= 902 && f <= 928) return '915MHz ISM';
if (f >= 315 && f <= 316) return '315MHz';
if (f >= 2402 && f <= 2480) return 'Bluetooth';
if (f >= 144 && f <= 148) return 'VHF Ham';
if (f >= 420 && f <= 450) return 'UHF Ham';
return `${f.toFixed(3)} MHz`;
}
}
// Bluetooth mode - MAC address
if (mode === 'bluetooth') {
if (/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(id)) {
return id.substring(0, 8) + '...';
}
}
// WiFi mode - SSID
if (mode === 'wifi') {
if (id.length > 20) {
return id.substring(0, 17) + '...';
}
}
return id;
}
/**
* Add or update an event
*/
function addEvent(containerId, eventData) {
const state = instances.get(containerId);
if (!state) return null;
const now = Date.now();
const id = eventData.id;
const label = eventData.label || generateLabel(id, state);
let item = state.items.get(id);
if (!item) {
item = createItem(id, label, {
type: eventData.type,
tags: eventData.tags,
metadata: eventData.metadata
});
item.firstSeen = now;
state.items.set(id, item);
addAnnotation(state, 'new', `New: ${item.label}`, now);
}
// Add event
item.events.push({
timestamp: now,
strength: Math.min(5, Math.max(1, eventData.strength || 3)),
duration: eventData.duration || 1000
});
item.lastSeen = now;
item.eventCount++;
// Update status
updateItemStatus(item, state.config);
// Detect patterns
detectPatterns(item, state);
// Prune old events
const windowMs = state.config.timeWindows['8h'] || state.config.timeWindows['2h'];
item.events = item.events.filter(e => now - e.timestamp < windowMs);
// Prune items if over limit
if (state.items.size > state.config.maxItems) {
pruneOldItems(state);
}
return item;
}
/**
* Bulk import events
*/
function importEvents(containerId, events) {
for (const event of events) {
addEvent(containerId, event);
}
}
/**
* Remove oldest/least active items
*/
function pruneOldItems(state) {
const items = Array.from(state.items.entries());
items.sort((a, b) => {
if (a[1].flagged && !b[1].flagged) return 1;
if (!a[1].flagged && b[1].flagged) return -1;
return a[1].lastSeen - b[1].lastSeen;
});
const toRemove = items.length - state.config.maxItems;
for (let i = 0; i < toRemove; i++) {
if (!items[i][1].flagged) {
state.items.delete(items[i][0]);
}
}
}
/**
* Update item status based on activity
*/
function updateItemStatus(item, config) {
const now = Date.now();
const recentEvents = item.events.filter(
e => now - e.timestamp < config.burstWindow
);
if (recentEvents.length >= config.burstThreshold) {
if (item.status !== 'burst') {
item.status = 'burst';
}
} else if (item.eventCount >= 20) {
item.status = 'baseline';
} else if (now - item.firstSeen < 5 * 60 * 1000) {
item.status = 'new';
}
if (item.flagged) {
item.status = 'flagged';
}
}
/**
* Detect repeating patterns
*/
function detectPatterns(item, state) {
if (item.events.length < 4) return;
const intervals = [];
for (let i = 1; i < item.events.length; i++) {
intervals.push(item.events[i].timestamp - item.events[i-1].timestamp);
}
if (intervals.length >= 3) {
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const tolerance = avgInterval * 0.1;
const consistent = intervals.filter(
i => Math.abs(i - avgInterval) <= tolerance
).length;
if (consistent >= intervals.length * 0.7) {
const seconds = Math.round(avgInterval / 1000);
if (seconds >= 1 && seconds <= 3600) {
const patternStr = seconds < 60
? `${seconds}s interval`
: `${Math.round(seconds/60)}m interval`;
if (item.pattern !== patternStr) {
item.pattern = patternStr;
addAnnotation(state, 'pattern', `Pattern: ${patternStr} - ${item.label}`, Date.now());
}
}
}
}
}
/**
* Add annotation
*/
function addAnnotation(state, type, message, timestamp) {
state.annotations.unshift({
type: type,
message: message,
timestamp: timestamp
});
if (state.annotations.length > 20) {
state.annotations.pop();
}
}
/**
* Toggle flag on item
*/
function toggleFlag(containerId, id) {
const state = instances.get(containerId);
if (!state) return;
const item = state.items.get(id);
if (item) {
item.flagged = !item.flagged;
item.status = item.flagged ? 'flagged' : 'new';
addAnnotation(state,
'flagged',
item.flagged ? `Flagged: ${item.label}` : `Unflagged: ${item.label}`,
Date.now()
);
if (state.config.onItemFlag) {
state.config.onItemFlag(item);
}
render(state);
}
}
/**
* Mark item as inactive
*/
function markInactive(containerId, id) {
const state = instances.get(containerId);
if (!state) return;
const item = state.items.get(id);
if (item && item.status !== 'gone') {
item.status = 'gone';
addAnnotation(state, 'gone', `Inactive: ${item.label}`, Date.now());
}
}
/**
* Build timeline DOM
*/
function buildTimeline(container, state) {
const config = state.config;
const timeline = document.createElement('div');
timeline.className = `activity-timeline activity-timeline--${config.visualMode}` +
(config.collapsed ? ' collapsed' : '');
timeline.id = `activityTimeline-${state.containerId}`;
timeline.dataset.mode = config.mode;
// Build filter buttons HTML
const filterButtonsHtml = buildFilterButtons(config);
// Build window options HTML
const windowOptionsHtml = config.availableWindows.map(w =>
``
).join('');
// Build legend HTML
const legendHtml = config.showLegend ? `
` : '';
timeline.innerHTML = `
${filterButtonsHtml}
Window:
◯
No activity recorded
Events will appear as they are detected
${legendHtml}
`;
container.appendChild(timeline);
return timeline;
}
/**
* Build filter buttons HTML
*/
function buildFilterButtons(config) {
let html = '';
for (const [key, filter] of Object.entries(config.filters)) {
if (filter.enabled) {
html += ``;
}
}
for (const filter of config.customFilters) {
html += ``;
}
return html;
}
/**
* Format window label
*/
function formatWindowLabel(window) {
const labels = {
'5m': '5 min',
'15m': '15 min',
'30m': '30 min',
'1h': '1 hour',
'2h': '2 hours',
'4h': '4 hours',
'8h': '8 hours'
};
return labels[window] || window;
}
/**
* Setup event listeners
*/
function setupEventListeners(state) {
const timeline = state.element;
// Collapse toggle
const header = timeline.querySelector('.activity-timeline-header');
if (header) {
header.addEventListener('click', (e) => {
if (e.target.closest('button') || e.target.closest('select')) return;
timeline.classList.toggle('collapsed');
});
}
// Filter buttons
timeline.querySelectorAll('.activity-timeline-btn[data-filter]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const filter = btn.dataset.filter;
state.filterState[filter] = !state.filterState[filter];
btn.classList.toggle('active', state.filterState[filter]);
render(state);
});
});
// Time window selector
const windowSelect = timeline.querySelector('.activity-timeline-window-select');
if (windowSelect) {
windowSelect.addEventListener('click', (e) => e.stopPropagation());
windowSelect.addEventListener('change', (e) => {
state.timeWindow = e.target.value;
render(state);
});
}
// Lane interactions
timeline.addEventListener('click', (e) => {
const lane = e.target.closest('.activity-timeline-lane');
if (lane && !e.target.closest('button')) {
lane.classList.toggle('expanded');
if (state.config.onItemClick) {
const id = lane.dataset.id;
const item = state.items.get(id);
if (item) {
state.config.onItemClick(item);
}
}
}
});
// Right-click to flag
timeline.addEventListener('contextmenu', (e) => {
const lane = e.target.closest('.activity-timeline-lane');
if (lane) {
e.preventDefault();
const id = lane.dataset.id;
toggleFlag(state.containerId, id);
}
});
}
/**
* Create tooltip element
*/
function createTooltip(state) {
if (state.tooltip) return;
state.tooltip = document.createElement('div');
state.tooltip.className = 'activity-timeline-tooltip';
state.tooltip.style.display = 'none';
document.body.appendChild(state.tooltip);
}
/**
* Show tooltip
*/
function showTooltip(e, item, state) {
if (!state.tooltip) return;
const lastSeenStr = formatTimeAgo(item.lastSeen);
state.tooltip.innerHTML = `
ID:
${item.id}
First seen:
${formatTime(item.firstSeen)}
Last seen:
${lastSeenStr}
Events:
${item.eventCount}
${item.pattern ? `
Pattern:
${item.pattern}
` : ''}
Status:
${item.status}
${item.tags.length > 0 ? `
Tags:
${item.tags.join(', ')}
` : ''}
`;
state.tooltip.style.display = 'block';
state.tooltip.style.left = (e.clientX + 10) + 'px';
state.tooltip.style.top = (e.clientY + 10) + 'px';
}
/**
* Hide tooltip
*/
function hideTooltip(state) {
if (state.tooltip) {
state.tooltip.style.display = 'none';
}
}
/**
* Start update timer
*/
function startUpdateTimer(state) {
if (state.updateTimer) {
clearInterval(state.updateTimer);
}
state.updateTimer = setInterval(() => {
render(state);
}, state.config.updateInterval);
}
/**
* Stop update timer
*/
function stopUpdateTimer(state) {
if (state.updateTimer) {
clearInterval(state.updateTimer);
state.updateTimer = null;
}
}
/**
* Render the timeline
*/
function render(state) {
const lanesContainer = state.element.querySelector('.activity-timeline-lanes');
const axisContainer = state.element.querySelector('.activity-timeline-axis');
const annotationsContainer = state.element.querySelector('.activity-timeline-annotations');
if (!lanesContainer) return;
const now = Date.now();
const windowMs = state.config.timeWindows[state.timeWindow];
const startTime = now - windowMs;
// Render time axis
renderAxis(axisContainer, startTime, now, windowMs);
// Get filtered items
let items = Array.from(state.items.values());
// Apply standard filters
if (state.filterState.hideBaseline) {
items = items.filter(s => s.status !== 'baseline');
}
if (state.filterState.showOnlyNew) {
items = items.filter(s => s.status === 'new');
}
if (state.filterState.showOnlyBurst) {
items = items.filter(s => s.status === 'burst');
}
// Apply custom filters
for (const filter of state.config.customFilters) {
if (state.filterState[filter.key] && filter.predicate) {
items = items.filter(filter.predicate);
}
}
// Sort by priority and recency
const statusPriority = { flagged: 0, burst: 1, new: 2, baseline: 3, gone: 4 };
items.sort((a, b) => {
const priorityDiff = statusPriority[a.status] - statusPriority[b.status];
if (priorityDiff !== 0) return priorityDiff;
return b.lastSeen - a.lastSeen;
});
// Render lanes
const totalItems = items.length;
const displayedItems = items.slice(0, state.config.maxDisplayedLanes);
const hiddenCount = totalItems - displayedItems.length;
if (items.length === 0) {
lanesContainer.innerHTML = `
◯
No activity recorded
Events will appear as they are detected
`;
} else {
let html = displayedItems.map(item =>
renderLane(item, startTime, now, windowMs, state)
).join('');
if (hiddenCount > 0) {
html += `
+${hiddenCount} more (scroll or adjust filters)
`;
}
lanesContainer.innerHTML = html;
// Add tooltip listeners
lanesContainer.querySelectorAll('.activity-timeline-lane').forEach(lane => {
const id = lane.dataset.id;
const item = state.items.get(id);
lane.addEventListener('mouseenter', (e) => showTooltip(e, item, state));
lane.addEventListener('mousemove', (e) => showTooltip(e, item, state));
lane.addEventListener('mouseleave', () => hideTooltip(state));
});
}
// Update header stats
const allItems = Array.from(state.items.values());
const statTotal = state.element.querySelector('[data-stat="total"]');
const statNew = state.element.querySelector('[data-stat="new"]');
const statBurst = state.element.querySelector('[data-stat="burst"]');
if (statTotal) statTotal.textContent = allItems.length;
if (statNew) statNew.textContent = allItems.filter(s => s.status === 'new').length;
if (statBurst) statBurst.textContent = allItems.filter(s => s.status === 'burst').length;
// Render annotations
if (state.config.showAnnotations) {
renderAnnotations(annotationsContainer, state);
}
}
/**
* Render time axis
*/
function renderAxis(container, startTime, endTime, windowMs) {
if (!container) return;
const labels = [];
const steps = 6;
for (let i = 0; i <= steps; i++) {
const time = startTime + (windowMs * i / steps);
const label = i === steps ? 'Now' : formatTimeShort(time);
labels.push(`${label}`);
}
container.innerHTML = labels.join('');
}
/**
* Render a single lane
*/
function renderLane(item, startTime, endTime, windowMs, state) {
const isBaseline = item.status === 'baseline';
const visualMode = state.config.visualMode;
// Get events within time window
const visibleEvents = item.events.filter(
e => e.timestamp >= startTime && e.timestamp <= endTime
);
// Generate bars
const barsHtml = aggregateAndRenderBars(visibleEvents, startTime, windowMs, state.config);
// Generate ticks for expanded view
const ticksHtml = visibleEvents.map(event => {
const position = ((event.timestamp - startTime) / windowMs) * 100;
return ``;
}).join('');
const recentCount = visibleEvents.length;
// Compact mode: minimal info
if (visualMode === 'compact') {
return `
${item.label}
${recentCount}
`;
}
// Enriched mode: full info
return `
${item.id}
${item.label}
${recentCount}
events
`;
}
/**
* Aggregate events into bars
*/
function aggregateAndRenderBars(events, startTime, windowMs, config) {
if (events.length === 0) return '';
const bars = [];
let currentBar = null;
const minGap = windowMs / 100;
events.sort((a, b) => a.timestamp - b.timestamp);
for (const event of events) {
if (!currentBar) {
currentBar = {
start: event.timestamp,
end: event.timestamp + event.duration,
maxStrength: event.strength,
count: 1
};
} else if (event.timestamp - currentBar.end <= minGap) {
currentBar.end = Math.max(currentBar.end, event.timestamp + event.duration);
currentBar.maxStrength = Math.max(currentBar.maxStrength, event.strength);
currentBar.count++;
} else {
bars.push(currentBar);
currentBar = {
start: event.timestamp,
end: event.timestamp + event.duration,
maxStrength: event.strength,
count: 1
};
}
}
if (currentBar) bars.push(currentBar);
return bars.map(bar => {
const left = ((bar.start - startTime) / windowMs) * 100;
const width = Math.max(
config.barMinWidth / 8,
((bar.end - bar.start) / windowMs) * 100
);
const status = bar.count >= config.burstThreshold ? 'burst' :
bar.count > 1 ? 'repeated' : 'new';
return ``;
}).join('');
}
/**
* Render annotations
*/
function renderAnnotations(container, state) {
if (!container) return;
const recentAnnotations = state.annotations.slice(0, 5);
if (recentAnnotations.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
const iconMap = {
new: '●',
burst: '◆',
pattern: '↻',
flagged: '⚑',
gone: '○'
};
container.innerHTML = recentAnnotations.map(ann => {
const icon = iconMap[ann.type] || '•';
return `
${icon}
${ann.message}
${formatTimeAgo(ann.timestamp)}
`;
}).join('');
}
/**
* Format time
*/
function formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
/**
* Format short time for axis
*/
function formatTimeShort(timestamp) {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
/**
* Format time ago
*/
function formatTimeAgo(timestamp) {
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 5) return 'just now';
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
return `${hours}h ago`;
}
/**
* Clear all data
*/
function clear(containerId) {
const state = instances.get(containerId);
if (!state) return;
state.items.clear();
state.annotations = [];
render(state);
}
/**
* Export data
*/
function exportData(containerId) {
const state = instances.get(containerId);
if (!state) return null;
const items = Array.from(state.items.values()).map(s => ({
id: s.id,
label: s.label,
type: s.type,
status: s.status,
pattern: s.pattern,
firstSeen: new Date(s.firstSeen).toISOString(),
lastSeen: new Date(s.lastSeen).toISOString(),
eventCount: s.eventCount,
flagged: s.flagged,
tags: s.tags
}));
return {
exportTime: new Date().toISOString(),
mode: state.config.mode,
timeWindow: state.timeWindow,
items: items,
annotations: state.annotations.map(a => ({
...a,
timestamp: new Date(a.timestamp).toISOString()
}))
};
}
/**
* Get stats
*/
function getStats(containerId) {
const state = instances.get(containerId);
if (!state) return null;
const items = Array.from(state.items.values());
return {
total: items.length,
new: items.filter(s => s.status === 'new').length,
baseline: items.filter(s => s.status === 'baseline').length,
burst: items.filter(s => s.status === 'burst').length,
flagged: items.filter(s => s.flagged).length,
withPattern: items.filter(s => s.pattern).length
};
}
/**
* Destroy instance
*/
function destroy(containerId) {
const state = instances.get(containerId);
if (!state) return;
stopUpdateTimer(state);
if (state.tooltip) {
state.tooltip.remove();
state.tooltip = null;
}
if (state.element) {
state.element.remove();
}
instances.delete(containerId);
}
/**
* Create public API for an instance
*/
function createPublicAPI(containerId) {
return {
// Data management
addEvent: (eventData) => addEvent(containerId, eventData),
importEvents: (events) => importEvents(containerId, events),
toggleFlag: (id) => toggleFlag(containerId, id),
markInactive: (id) => markInactive(containerId, id),
clear: () => clear(containerId),
// Rendering
render: () => {
const state = instances.get(containerId);
if (state) render(state);
},
// Data access
getItems: () => {
const state = instances.get(containerId);
return state ? Array.from(state.items.values()) : [];
},
getAnnotations: () => {
const state = instances.get(containerId);
return state ? state.annotations : [];
},
getStats: () => getStats(containerId),
exportData: () => exportData(containerId),
// Configuration
setTimeWindow: (window) => {
const state = instances.get(containerId);
if (state && state.config.timeWindows[window]) {
state.timeWindow = window;
render(state);
}
},
setFilter: (filter, value) => {
const state = instances.get(containerId);
if (state && state.filterState.hasOwnProperty(filter)) {
state.filterState[filter] = value;
render(state);
}
},
// Lifecycle
destroy: () => destroy(containerId)
};
}
// Global API
return {
create: create,
// Convenience methods for single-instance use
addEvent: (containerId, eventData) => addEvent(containerId, eventData),
clear: (containerId) => clear(containerId),
getStats: (containerId) => getStats(containerId),
exportData: (containerId) => exportData(containerId),
destroy: (containerId) => destroy(containerId),
// Instance access
getInstance: (containerId) => instances.get(containerId),
getInstances: () => instances
};
})();
// Backwards compatibility alias
window.ActivityTimeline = ActivityTimeline;
// Legacy SignalTimeline compatibility wrapper
window.SignalTimeline = (function() {
'use strict';
let legacyInstance = null;
const LEGACY_CONTAINER = 'signalTimelineContainer';
return {
create: function(containerId, options = {}) {
// Map old options to new format
const newOptions = {
title: 'Signal Activity Timeline',
mode: 'tscm',
visualMode: 'enriched',
collapsed: options.collapsed !== false,
showAnnotations: true,
showLegend: true
};
legacyInstance = ActivityTimeline.create(containerId, newOptions);
return legacyInstance ? document.getElementById(`activityTimeline-${containerId}`) : null;
},
destroy: function() {
if (legacyInstance) {
legacyInstance.destroy();
legacyInstance = null;
}
},
addEvent: function(frequency, strength = 3, duration = 1000, name = null) {
if (legacyInstance) {
return legacyInstance.addEvent({
id: String(frequency),
label: name,
strength: strength,
duration: duration,
type: 'rf'
});
}
return null;
},
flagSignal: function(frequency) {
if (legacyInstance) {
legacyInstance.toggleFlag(String(frequency));
}
},
markGone: function(frequency) {
if (legacyInstance) {
legacyInstance.markInactive(String(frequency));
}
},
clear: function() {
if (legacyInstance) {
legacyInstance.clear();
}
},
render: function() {
if (legacyInstance) {
legacyInstance.render();
}
},
getSignals: function() {
return legacyInstance ? legacyInstance.getItems() : [];
},
getAnnotations: function() {
return legacyInstance ? legacyInstance.getAnnotations() : [];
},
getStats: function() {
return legacyInstance ? legacyInstance.getStats() : null;
},
exportData: function() {
return legacyInstance ? legacyInstance.exportData() : null;
},
setTimeWindow: function(window) {
if (legacyInstance) {
legacyInstance.setTimeWindow(window);
}
},
setFilter: function(filter, value) {
if (legacyInstance) {
legacyInstance.setFilter(filter, value);
}
}
};
})();