CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-finos--perspective-viewer

The `<perspective-viewer>` Custom Element, frontend for Perspective.js, providing interactive analytics and data visualization.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

events.mddocs/

Event System

Custom events dispatched by the perspective-viewer for user interactions and configuration changes.

Capabilities

Event Registration

Register event listeners for perspective-viewer custom events.

/**
 * Add event listener for configuration updates
 */
addEventListener(
  name: "perspective-config-update",
  cb: (e: CustomEvent<ViewerConfigUpdate>) => void,
  options?: { signal: AbortSignal }
): void;

/**
 * Add event listener for data selection changes
 */
addEventListener(
  name: "perspective-select", 
  cb: (e: CustomEvent<PerspectiveSelectEventDetail>) => void,
  options?: { signal: AbortSignal }
): void;

/**
 * Add event listener for settings panel toggle
 */
addEventListener(
  name: "perspective-toggle-settings",
  cb: (e: CustomEvent<boolean>) => void,
  options?: { signal: AbortSignal }
): void;

/**
 * Add event listener for column settings toggle
 */
addEventListener(
  name: "perspective-toggle-column-settings",
  cb: (e: CustomEvent<{open: boolean, column_name?: string}>) => void,
  options?: { signal: AbortSignal }
): void;

/**
 * Add event listener for user click events on data
 */
addEventListener(
  name: "perspective-click",
  cb: (e: CustomEvent<PerspectiveClickEventDetail>) => void,
  options?: { signal: AbortSignal }
): void;

/**
 * Remove event listeners
 */
removeEventListener(name: "perspective-config-update", cb: any): void;
removeEventListener(name: "perspective-select", cb: any): void;
removeEventListener(name: "perspective-toggle-settings", cb: any): void;
removeEventListener(name: "perspective-toggle-column-settings", cb: any): void;
removeEventListener(name: "perspective-click", cb: any): void;

Configuration Update Events

Fired when the viewer configuration changes through user interaction or programmatic updates.

/**
 * Event detail for configuration updates
 * Contains the complete ViewerConfigUpdate object
 */
interface PerspectiveConfigUpdateEvent extends CustomEvent {
  detail: ViewerConfigUpdate;
}

Usage:

import "@finos/perspective-viewer";

const viewer = document.createElement("perspective-viewer");

viewer.addEventListener("perspective-config-update", (event) => {
  const config = event.detail;
  console.log("Configuration changed:", config);
  
  // Save configuration automatically
  localStorage.setItem("viewerConfig", JSON.stringify(config));
  
  // React to specific changes
  if (config.plugin) {
    console.log(`Plugin changed to: ${config.plugin}`);
  }
  
  if (config.filter) {
    console.log(`Filters applied: ${config.filter.length}`);
  }
});

Selection Events

Fired when user selects data in the viewer or selection is changed programmatically.

/**
 * Event detail for selection changes
 */
interface PerspectiveSelectEventDetail {
  view_window: ViewWindow;
}

interface ViewWindow {
  start_row: number;
  end_row: number;
  start_col: number;
  end_col: number;
}

Usage:

viewer.addEventListener("perspective-select", (event) => {
  const { view_window } = event.detail;
  
  console.log(`Selected rows: ${view_window.start_row} to ${view_window.end_row}`);
  console.log(`Selected columns: ${view_window.start_col} to ${view_window.end_col}`);
  
  // Get selected data
  viewer.getView().then(async (view) => {
    const selectedData = await view.to_json({
      start_row: view_window.start_row,
      end_row: view_window.end_row,
      start_col: view_window.start_col,
      end_col: view_window.end_col
    });
    
    console.log("Selected data:", selectedData);
  });
});

// Clear selection
viewer.addEventListener("perspective-select", (event) => {
  if (!event.detail.view_window) {
    console.log("Selection cleared");
  }
});

Click Events

Fired when user clicks on data cells or visualization elements.

/**
 * Event detail for click events
 * Contains clicked row data and configuration context
 */
interface PerspectiveClickEvent extends CustomEvent {
  detail: PerspectiveClickEventDetail;
}

Usage:

viewer.addEventListener("perspective-click", (event) => {
  const { config, row, column_names } = event.detail;
  
  console.log("Data clicked:", row);
  console.log("Column names:", column_names);
  console.log("Current config:", config);
  
  // Example: Show details in a popup
  showDetailsPopup(row, column_names);
  
  // Example: Filter other viewers based on clicked data
  filterOtherViewers(row);
  
  // Example: Navigate to detail page
  const id = row.id;
  if (id) {
    window.location.href = `/details/${id}`;
  }
});

function showDetailsPopup(row: Record<string, any>, columns: Array<string | Array<string>>) {
  const popup = document.createElement("div");
  popup.className = "data-popup";
  
  const content = Object.entries(row)
    .map(([key, value]) => `<div><strong>${key}:</strong> ${value}</div>`)
    .join("");
    
  popup.innerHTML = `
    <h3>Row Details</h3>
    ${content}
    <button onclick="this.parentElement.remove()">Close</button>
  `;
  
  document.body.appendChild(popup);
}

Settings Panel Events

Fired when the settings panel is opened or closed.

/**
 * Event detail for settings panel toggle
 * Boolean indicates whether panel is open (true) or closed (false)
 */
interface PerspectiveToggleSettingsEvent extends CustomEvent {
  detail: boolean;
}

Usage:

viewer.addEventListener("perspective-toggle-settings", (event) => {
  const isOpen = event.detail;
  
  if (isOpen) {
    console.log("Settings panel opened");
    // Maybe adjust layout or show help text
  } else {
    console.log("Settings panel closed");
    // Maybe hide help text or expand chart area
  }
});

Column Settings Events

Fired when column-specific settings panel is opened or closed.

/**
 * Event detail for column settings toggle
 */
interface PerspectiveToggleColumnSettingsEvent extends CustomEvent {
  detail: {
    open: boolean;
    column_name?: string;
  };
}

Usage:

viewer.addEventListener("perspective-toggle-column-settings", (event) => {
  const { open, column_name } = event.detail;
  
  if (open && column_name) {
    console.log(`Column settings opened for: ${column_name}`);
    // Show column-specific help or information
  } else {
    console.log("Column settings closed");
  }
});

Event Handling Examples

Configuration Synchronization

import "@finos/perspective-viewer";

class DashboardManager {
  private viewers: HTMLPerspectiveViewerElement[] = [];
  
  constructor() {
    this.setupEventHandlers();
  }
  
  addViewer(viewer: HTMLPerspectiveViewerElement) {
    this.viewers.push(viewer);
    this.setupViewerEvents(viewer);
  }
  
  private setupViewerEvents(viewer: HTMLPerspectiveViewerElement) {
    // Sync theme changes across all viewers
    viewer.addEventListener("perspective-config-update", (event) => {
      const config = event.detail;
      
      if (config.theme) {
        this.synchronizeTheme(config.theme, viewer);
      }
    });
    
    // Handle selection for cross-filtering
    viewer.addEventListener("perspective-select", (event) => {
      this.handleCrossFilter(event.detail.view_window, viewer);
    });
  }
  
  private synchronizeTheme(theme: string, sourceViewer: HTMLPerspectiveViewerElement) {
    this.viewers.forEach(async (viewer) => {
      if (viewer !== sourceViewer) {
        const config = await viewer.save();
        await viewer.restore({ ...config, theme });
      }
    });
  }
  
  private async handleCrossFilter(selection: ViewWindow, sourceViewer: HTMLPerspectiveViewerElement) {
    // Get selected data from source viewer
    const sourceView = await sourceViewer.getView();
    const selectedData = await sourceView.to_json({
      start_row: selection.start_row,
      end_row: selection.end_row
    });
    
    // Apply filters to other viewers based on selection
    // (Implementation depends on your cross-filtering logic)
  }
}

Auto-Save Configuration

class AutoSaveManager {
  private saveTimeout: number | null = null;
  private readonly SAVE_DELAY = 1000; // 1 second debounce
  
  constructor(private viewer: HTMLPerspectiveViewerElement, private storageKey: string) {
    this.setupAutoSave();
  }
  
  private setupAutoSave() {
    this.viewer.addEventListener("perspective-config-update", (event) => {
      // Debounce saves to avoid excessive storage writes
      if (this.saveTimeout) {
        clearTimeout(this.saveTimeout);
      }
      
      this.saveTimeout = window.setTimeout(() => {
        this.saveConfiguration(event.detail);
      }, this.SAVE_DELAY);
    });
  }
  
  private saveConfiguration(config: ViewerConfigUpdate) {
    try {
      localStorage.setItem(this.storageKey, JSON.stringify(config));
      console.log("Configuration auto-saved");
    } catch (error) {
      console.error("Failed to save configuration:", error);
    }
  }
  
  async loadConfiguration(): Promise<boolean> {
    try {
      const saved = localStorage.getItem(this.storageKey);
      if (saved) {
        const config = JSON.parse(saved);
        await this.viewer.restore(config);
        return true;
      }
    } catch (error) {
      console.error("Failed to load configuration:", error);
    }
    return false;
  }
}

// Usage
const viewer = document.createElement("perspective-viewer");
const autoSave = new AutoSaveManager(viewer, "dashboard-config");

// Load saved configuration on startup
autoSave.loadConfiguration();

Selection-Based Actions

class SelectionHandler {
  constructor(private viewer: HTMLPerspectiveViewerElement) {
    this.setupSelectionHandler();
  }
  
  private setupSelectionHandler() {
    this.viewer.addEventListener("perspective-select", async (event) => {
      const { view_window } = event.detail;
      
      if (view_window) {
        await this.handleSelection(view_window);
      } else {
        this.clearSelection();
      }
    });
  }
  
  private async handleSelection(selection: ViewWindow) {
    const view = await this.viewer.getView();
    
    // Get selected data
    const selectedData = await view.to_json({
      start_row: selection.start_row,
      end_row: selection.end_row,
      start_col: selection.start_col,
      end_col: selection.end_col
    });
    
    // Show selection details
    this.showSelectionDetails(selectedData, selection);
    
    // Enable selection-based actions
    this.enableSelectionActions(selectedData);
  }
  
  private showSelectionDetails(data: any[], selection: ViewWindow) {
    const rowCount = selection.end_row - selection.start_row + 1;
    const colCount = selection.end_col - selection.start_col + 1;
    
    console.log(`Selected ${rowCount} rows and ${colCount} columns`);
    console.log("Selected data:", data);
    
    // Update UI to show selection info
    document.getElementById("selection-info")!.textContent = 
      `${rowCount} rows × ${colCount} columns selected`;
  }
  
  private enableSelectionActions(data: any[]) {
    // Enable export selected data button
    const exportBtn = document.getElementById("export-selection") as HTMLButtonElement;
    exportBtn.disabled = false;
    exportBtn.onclick = () => this.exportSelection(data);
    
    // Enable other selection-based actions
    this.setupDetailView(data);
  }
  
  private clearSelection() {
    document.getElementById("selection-info")!.textContent = "No selection";
    (document.getElementById("export-selection") as HTMLButtonElement).disabled = true;
  }
  
  private exportSelection(data: any[]) {
    const csv = this.convertToCSV(data);
    this.downloadCSV(csv, "selection.csv");
  }
  
  private convertToCSV(data: any[]): string {
    if (data.length === 0) return "";
    
    const headers = Object.keys(data[0]);
    const csvContent = [
      headers.join(","),
      ...data.map(row => headers.map(header => `"${row[header]}"`).join(","))
    ].join("\n");
    
    return csvContent;
  }
  
  private downloadCSV(content: string, filename: string) {
    const blob = new Blob([content], { type: "text/csv" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(url);
  }
  
  private setupDetailView(data: any[]) {
    // Create detailed view of selected data
    // (Implementation depends on your UI framework)
  }
}

Event Cleanup

class EventManager {
  private abortController = new AbortController();
  
  constructor(private viewer: HTMLPerspectiveViewerElement) {
    this.setupEvents();
  }
  
  private setupEvents() {
    const { signal } = this.abortController;
    
    // All event listeners will be automatically removed when aborted
    this.viewer.addEventListener("perspective-config-update", (event) => {
      this.handleConfigUpdate(event.detail);
    }, { signal });
    
    this.viewer.addEventListener("perspective-select", (event) => {
      this.handleSelection(event.detail);
    }, { signal });
    
    this.viewer.addEventListener("perspective-toggle-settings", (event) => {
      this.handleSettingsToggle(event.detail);
    }, { signal });
  }
  
  private handleConfigUpdate(config: ViewerConfigUpdate) {
    // Handle configuration updates
  }
  
  private handleSelection(detail: PerspectiveSelectEventDetail) {
    // Handle selection changes
  }
  
  private handleSettingsToggle(isOpen: boolean) {
    // Handle settings panel toggle
  }
  
  // Clean up all event listeners
  destroy() {
    this.abortController.abort();
  }
}

// Usage
const viewer = document.createElement("perspective-viewer");
const eventManager = new EventManager(viewer);

// Clean up when component unmounts
window.addEventListener("beforeunload", () => {
  eventManager.destroy();
});

Event Types Reference

// All event detail types
type PerspectiveConfigUpdateEvent = CustomEvent<ViewerConfigUpdate>;
type PerspectiveSelectEvent = CustomEvent<PerspectiveSelectEventDetail>;
type PerspectiveClickEvent = CustomEvent<PerspectiveClickEventDetail>;
type PerspectiveToggleSettingsEvent = CustomEvent<boolean>;
type PerspectiveToggleColumnSettingsEvent = CustomEvent<{
  open: boolean;
  column_name?: string;
}>;

// Event detail interfaces
interface PerspectiveClickEventDetail {
  config: ViewerConfigUpdate;
  row: Record<string, any>;
  column_names: Array<string | Array<string>>;
}

// Event listener type helpers
type ConfigUpdateListener = (event: PerspectiveConfigUpdateEvent) => void;
type SelectListener = (event: PerspectiveSelectEvent) => void;
type ClickListener = (event: PerspectiveClickEvent) => void;
type SettingsToggleListener = (event: PerspectiveToggleSettingsEvent) => void;
type ColumnSettingsToggleListener = (event: PerspectiveToggleColumnSettingsEvent) => void;

docs

configuration.md

core-element.md

events.md

index.md

plugin-development.md

wasm-init.md

tile.json