Custom Objects

Create powerful custom visualizations and interactive objects that extend Qlik's native capabilities with your own business logic and styling.

Custom Objects Overview

Custom objects in Qlik allow you to create specialized visualizations, interactive components, and business-specific analytics that go beyond standard charts and tables.

Session Objects
Temporary objects for analysis and calculation
Visualizations
Custom charts and visual components
Interactions
Interactive elements with custom behavior
Extensions
Reusable custom object templates

Session Objects

Creating Session Objects

Build temporary objects for custom analysis and calculations

typescript
class CustomSessionObjects {
  private app: any;

  constructor(app: any) {
    this.app = app;
  }

  // Create a custom KPI calculator
  async createKPICalculator(config: {
    title: string;
    measure: string;
    comparisonPeriod?: string;
    threshold?: { warning: number; critical: number };
  }): Promise<any> {
    
    const kpiDefinition = {
      qInfo: {
        qType: 'custom-kpi',
        qId: `kpi-${Date.now()}`
      },
      qHyperCubeDef: {
        qDimensions: [],
        qMeasures: [
          {
            qDef: {
              qDef: config.measure,
              qLabel: config.title
            }
          }
        ],
        qInitialDataFetch: [
          {
            qLeft: 0,
            qTop: 0,
            qWidth: 1,
            qHeight: 1
          }
        ]
      },
      // Custom properties for KPI behavior
      customProperties: {
        title: config.title,
        threshold: config.threshold,
        comparisonPeriod: config.comparisonPeriod
      }
    };

    // Add comparison measure if specified
    if (config.comparisonPeriod) {
      kpiDefinition.qHyperCubeDef.qMeasures.push({
        qDef: {
          qDef: `${config.measure.replace(/}/g, ', Period={${config.comparisonPeriod}}}`)},
          qLabel: `${config.title} (Previous)`
        }
      });
      
      // Add variance calculation
      kpiDefinition.qHyperCubeDef.qMeasures.push({
        qDef: {
          qDef: `(${config.measure} - ${config.measure.replace(/}/g, ', Period={${config.comparisonPeriod}}}')}) / ${config.measure.replace(/}/g, ', Period={${config.comparisonPeriod}}'})`,
          qLabel: 'Variance %'
        }
      });
    }

    const sessionObject = await this.app.createSessionObject(kpiDefinition);
    console.log(`✅ KPI calculator created: ${config.title}`);
    
    return sessionObject;
  }

  // Create a custom trend analyzer
  async createTrendAnalyzer(config: {
    dimensions: string[];
    measure: string;
    periods: number;
    trendType: 'linear' | 'polynomial' | 'exponential';
  }): Promise<any> {

    const trendDefinition = {
      qInfo: {
        qType: 'custom-trend-analyzer',
        qId: `trend-${Date.now()}`
      },
      qHyperCubeDef: {
        qDimensions: config.dimensions.map(dim => ({
          qDef: {
            qFieldDefs: [dim],
            qSortBy: { qSortByAscii: 1 }
          }
        })),
        qMeasures: [
          {
            qDef: {
              qDef: config.measure,
              qLabel: 'Current Value'
            }
          },
          {
            qDef: {
              qDef: this.getTrendCalculation(config.measure, config.trendType),
              qLabel: 'Trend Value'
            }
          },
          {
            qDef: {
              qDef: `If(${this.getTrendCalculation(config.measure, config.trendType)} > ${config.measure}, '↗️', If(${this.getTrendCalculation(config.measure, config.trendType)} < ${config.measure}, '↘️', '➡️'))`,
              qLabel: 'Trend Direction'
            }
          }
        ],
        qInitialDataFetch: [
          {
            qLeft: 0,
            qTop: 0,
            qWidth: config.dimensions.length + 3,
            qHeight: 1000
          }
        ]
      },
      customProperties: {
        trendType: config.trendType,
        periods: config.periods
      }
    };

    const sessionObject = await this.app.createSessionObject(trendDefinition);
    console.log('✅ Trend analyzer created');
    
    return sessionObject;
  }

  // Create a custom ranking object
  async createRankingObject(config: {
    dimension: string;
    measure: string;
    topN: number;
    includeOthers: boolean;
  }): Promise<any> {

    const rankingDefinition = {
      qInfo: {
        qType: 'custom-ranking',
        qId: `ranking-${Date.now()}`
      },
      qHyperCubeDef: {
        qDimensions: [
          {
            qDef: {
              qFieldDefs: [config.dimension],
              qSortBy: { qSortByLoadOrder: 1 }
            },
            qOtherTotalSpec: config.includeOthers ? {
              qOtherMode: 'OTHER_COUNTED',
              qOtherCounted: { qv: config.topN.toString() }
            } : undefined
          }
        ],
        qMeasures: [
          {
            qDef: {
              qDef: config.measure,
              qLabel: 'Value'
            },
            qSortBy: { qSortByNumeric: -1 } // Descending order
          },
          {
            qDef: {
              qDef: `Rank(${config.measure})`,
              qLabel: 'Rank'
            }
          },
          {
            qDef: {
              qDef: `(${config.measure} / Sum(Total ${config.measure})) * 100`,
              qLabel: 'Percentage'
            }
          }
        ],
        qInitialDataFetch: [
          {
            qLeft: 0,
            qTop: 0,
            qWidth: 4,
            qHeight: config.includeOthers ? config.topN + 1 : config.topN
          }
        ]
      }
    };

    const sessionObject = await this.app.createSessionObject(rankingDefinition);
    console.log('✅ Ranking object created');
    
    return sessionObject;
  }

  private getTrendCalculation(measure: string, trendType: string): string {
    switch (trendType) {
      case 'linear':
        return `Avg(${measure})`; // Simplified linear trend
      case 'polynomial':
        return `Median(${measure})`; // Simplified polynomial trend
      case 'exponential':
        return `Exp(Avg(Log(${measure})))`; // Geometric mean for exponential
      default:
        return measure;
    }
  }
}

// Usage Examples
async function createCustomAnalytics(app: any) {
  const customObjects = new CustomSessionObjects(app);

  // 1. Sales KPI with comparison
  const salesKPI = await customObjects.createKPICalculator({
    title: 'Monthly Sales',
    measure: 'Sum({<Month={$(=Max(Month))}>} Sales)',
    comparisonPeriod: 'Previous Month',
    threshold: { warning: 80, critical: 60 }
  });

  // 2. Customer growth trend
  const customerTrend = await customObjects.createTrendAnalyzer({
    dimensions: ['Month'],
    measure: 'Count(distinct Customer)',
    periods: 12,
    trendType: 'linear'
  });

  // 3. Top products ranking
  const productRanking = await customObjects.createRankingObject({
    dimension: 'Product',
    measure: 'Sum(Sales)',
    topN: 10,
    includeOthers: true
  });

  return { salesKPI, customerTrend, productRanking };
}

Custom Visualizations

Building Custom Charts

Create interactive visualizations with D3.js, Chart.js, or other libraries

typescript
class CustomVisualizationBuilder {
  private app: any;

  constructor(app: any) {
    this.app = app;
  }

  // Create a custom D3.js visualization
  async createD3Visualization(config: {
    containerId: string;
    sessionObject: any;
    chartType: 'bubble' | 'network' | 'treemap' | 'sankey';
    options?: any;
  }): Promise<void> {

    const layout = await config.sessionObject.getLayout();
    const data = await this.extractVisualizationData(config.sessionObject);
    
    const container = document.getElementById(config.containerId);
    if (!container) {
      throw new Error(`Container not found: ${config.containerId}`);
    }

    switch (config.chartType) {
      case 'bubble':
        await this.createBubbleChart(container, data, config.options);
        break;
      case 'network':
        await this.createNetworkChart(container, data, config.options);
        break;
      case 'treemap':
        await this.createTreemapChart(container, data, config.options);
        break;
      case 'sankey':
        await this.createSankeyChart(container, data, config.options);
        break;
    }

    console.log(`✅ ${config.chartType} visualization created`);
  }

  private async createBubbleChart(container: HTMLElement, data: any, options: any): Promise<void> {
    // This would integrate with D3.js
    const bubbleData = data.map((row: any, index: number) => ({
      id: index,
      name: row[0]?.text || `Item ${index}`,
      x: row[1]?.number || 0,
      y: row[2]?.number || 0,
      size: row[3]?.number || 10,
      category: row[4]?.text || 'default'
    }));

    // Simplified D3.js bubble chart implementation
    container.innerHTML = `
      <svg width="${options?.width || 600}" height="${options?.height || 400}">
        ${bubbleData.map((d: any, i: number) => `
          <circle 
            cx="${50 + (d.x % 500)}" 
            cy="${50 + (d.y % 300)}" 
            r="${Math.max(5, d.size / 10)}" 
            fill="${this.getColorByCategory(d.category)}"
            stroke="#333"
            stroke-width="1"
            opacity="0.7"
            title="${d.name}: ${d.size}"
          />
          <text 
            x="${50 + (d.x % 500)}" 
            y="${55 + (d.y % 300)}" 
            text-anchor="middle" 
            font-size="10"
            fill="#333"
          >${d.name}</text>
        `).join('')}
      </svg>
    `;
  }

  private async createNetworkChart(container: HTMLElement, data: any, options: any): Promise<void> {
    // Network/Graph visualization
    const nodes = new Set();
    const links: any[] = [];

    data.forEach((row: any) => {
      const source = row[0]?.text;
      const target = row[1]?.text;
      const weight = row[2]?.number || 1;

      if (source && target) {
        nodes.add(source);
        nodes.add(target);
        links.push({ source, target, weight });
      }
    });

    const networkData = {
      nodes: Array.from(nodes).map(node => ({ id: node, name: node })),
      links: links
    };

    // Simplified network visualization
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', (options?.width || 600).toString());
    svg.setAttribute('height', (options?.height || 400).toString());
    
    // Add basic network visualization here
    container.innerHTML = '';
    container.appendChild(svg);
  }

  private async createTreemapChart(container: HTMLElement, data: any, options: any): Promise<void> {
    // Treemap visualization for hierarchical data
    const hierarchyData = this.buildHierarchy(data);
    
    container.innerHTML = `
      <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 2px; height: ${options?.height || 400}px;">
        ${hierarchyData.map((item: any) => `
          <div style="
            background-color: ${this.getColorByCategory(item.category)};
            padding: 8px;
            border-radius: 4px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: ${Math.max(50, item.value / 100)}px;
            opacity: 0.8;
          ">
            <div style="font-weight: bold; font-size: 12px; text-align: center;">${item.name}</div>
            <div style="font-size: 10px; color: #666;">${item.value}</div>
          </div>
        `).join('')}
      </div>
    `;
  }

  private async createSankeyChart(container: HTMLElement, data: any, options: any): Promise<void> {
    // Sankey diagram for flow visualization
    const flowData = this.processFlowData(data);
    
    // Simplified Sankey representation
    container.innerHTML = `
      <div style="display: flex; flex-direction: column; gap: 4px; height: ${options?.height || 400}px;">
        ${flowData.map((flow: any) => `
          <div style="
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 8px;
            background-color: #f0f0f0;
            border-radius: 4px;
          ">
            <div style="
              background-color: ${this.getColorByCategory(flow.source)};
              padding: 4px 8px;
              border-radius: 2px;
              font-size: 12px;
            ">${flow.source}</div>
            <div style="
              flex: 1;
              height: 4px;
              background: linear-gradient(to right, ${this.getColorByCategory(flow.source)}, ${this.getColorByCategory(flow.target)});
            "></div>
            <div style="font-size: 10px; color: #666;">${flow.value}</div>
            <div style="
              background-color: ${this.getColorByCategory(flow.target)};
              padding: 4px 8px;
              border-radius: 2px;
              font-size: 12px;
            ">${flow.target}</div>
          </div>
        `).join('')}
      </div>
    `;
  }

  private async extractVisualizationData(sessionObject: any): Promise<any[]> {
    const layout = await sessionObject.getLayout();
    const hypercube = layout.qHyperCube;
    
    if (!hypercube) return [];
    
    return hypercube.qDataPages[0]?.qMatrix || [];
  }

  private buildHierarchy(data: any[]): any[] {
    return data.map((row, index) => ({
      name: row[0]?.text || `Item ${index}`,
      value: row[1]?.number || 0,
      category: row[2]?.text || 'default'
    })).sort((a, b) => b.value - a.value);
  }

  private processFlowData(data: any[]): any[] {
    return data.map(row => ({
      source: row[0]?.text || 'Source',
      target: row[1]?.text || 'Target',
      value: row[2]?.number || 1
    }));
  }

  private getColorByCategory(category: string): string {
    const colors: Record<string, string> = {
      'default': '#3498db',
      'sales': '#e74c3c',
      'profit': '#2ecc71',
      'customer': '#f39c12',
      'product': '#9b59b6'
    };
    
    return colors[category.toLowerCase()] || colors['default'];
  }
}

// Usage
async function createCustomVisualizations(app: any) {
  const vizBuilder = new CustomVisualizationBuilder(app);

  // Create data objects for visualizations
  const bubbleData = await app.createSessionObject({
    qInfo: { qType: 'bubble-data' },
    qHyperCubeDef: {
      qDimensions: [{ qDef: { qFieldDefs: ['Product'] } }],
      qMeasures: [
        { qDef: { qDef: 'Sum(Sales)' } },
        { qDef: { qDef: 'Sum(Profit)' } },
        { qDef: { qDef: 'Count(OrderID)' } }
      ],
      qInitialDataFetch: [{ qLeft: 0, qTop: 0, qWidth: 4, qHeight: 100 }]
    }
  });

  // Create bubble chart
  await vizBuilder.createD3Visualization({
    containerId: 'bubble-chart-container',
    sessionObject: bubbleData,
    chartType: 'bubble',
    options: { width: 800, height: 500 }
  });

  console.log('✅ Custom visualizations created');
}

Object Lifecycle Management

Managing Custom Object Lifecycle

Best practices for creating, updating, and destroying custom objects

typescript
class CustomObjectManager {
  private app: any;
  private managedObjects = new Map<string, any>();

  constructor(app: any) {
    this.app = app;
  }

  // Create and register a managed object
  async createManagedObject(id: string, definition: any): Promise<any> {
    try {
      const sessionObject = await this.app.createSessionObject(definition);
      
      // Store reference for management
      this.managedObjects.set(id, {
        object: sessionObject,
        definition: definition,
        created: new Date(),
        lastUpdated: new Date()
      });

      console.log(`✅ Managed object created: ${id}`);
      return sessionObject;
      
    } catch (error) {
      console.error(`Failed to create managed object ${id}:`, error);
      throw error;
    }
  }

  // Update existing object
  async updateManagedObject(id: string, newDefinition: any): Promise<any> {
    const managed = this.managedObjects.get(id);
    
    if (!managed) {
      throw new Error(`Managed object not found: ${id}`);
    }

    try {
      // Update the object properties
      await managed.object.setProperties(newDefinition);
      
      // Update our tracking
      managed.definition = newDefinition;
      managed.lastUpdated = new Date();
      
      console.log(`✅ Managed object updated: ${id}`);
      return managed.object;
      
    } catch (error) {
      console.error(`Failed to update managed object ${id}:`, error);
      throw error;
    }
  }

  // Get managed object
  getManagedObject(id: string): any | null {
    const managed = this.managedObjects.get(id);
    return managed ? managed.object : null;
  }

  // Destroy specific object
  async destroyManagedObject(id: string): Promise<void> {
    const managed = this.managedObjects.get(id);
    
    if (!managed) {
      console.warn(`Managed object not found: ${id}`);
      return;
    }

    try {
      await managed.object.destroy();
      this.managedObjects.delete(id);
      console.log(`✅ Managed object destroyed: ${id}`);
      
    } catch (error) {
      console.error(`Failed to destroy managed object ${id}:`, error);
    }
  }

  // Destroy all managed objects
  async destroyAllManagedObjects(): Promise<void> {
    const destroyPromises = Array.from(this.managedObjects.keys()).map(id => 
      this.destroyManagedObject(id)
    );

    await Promise.all(destroyPromises);
    console.log('✅ All managed objects destroyed');
  }

  // Get object statistics
  getObjectStatistics(): any {
    const stats = {
      totalObjects: this.managedObjects.size,
      objectsByType: new Map<string, number>(),
      oldestObject: null as Date | null,
      newestObject: null as Date | null
    };

    for (const [id, managed] of this.managedObjects) {
      // Count by type
      const type = managed.definition.qInfo?.qType || 'unknown';
      stats.objectsByType.set(type, (stats.objectsByType.get(type) || 0) + 1);

      // Track creation dates
      if (!stats.oldestObject || managed.created < stats.oldestObject) {
        stats.oldestObject = managed.created;
      }
      if (!stats.newestObject || managed.created > stats.newestObject) {
        stats.newestObject = managed.created;
      }
    }

    return stats;
  }

  // Auto-cleanup old objects
  async cleanupOldObjects(maxAgeHours: number = 24): Promise<number> {
    const cutoffTime = new Date(Date.now() - (maxAgeHours * 60 * 60 * 1000));
    const objectsToDestroy: string[] = [];

    for (const [id, managed] of this.managedObjects) {
      if (managed.created < cutoffTime) {
        objectsToDestroy.push(id);
      }
    }

    for (const id of objectsToDestroy) {
      await this.destroyManagedObject(id);
    }

    console.log(`✅ Cleaned up ${objectsToDestroy.length} old objects`);
    return objectsToDestroy.length;
  }
}

// React Hook for Object Management
function useCustomObjectManager(app: any) {
  const [objectManager] = useState(() => new CustomObjectManager(app));
  const [objects, setObjects] = useState<Map<string, any>>(new Map());

  useEffect(() => {
    // Cleanup on unmount
    return () => {
      objectManager.destroyAllManagedObjects();
    };
  }, [objectManager]);

  const createObject = useCallback(async (id: string, definition: any) => {
    const obj = await objectManager.createManagedObject(id, definition);
    setObjects(new Map(objectManager['managedObjects']));
    return obj;
  }, [objectManager]);

  const destroyObject = useCallback(async (id: string) => {
    await objectManager.destroyManagedObject(id);
    setObjects(new Map(objectManager['managedObjects']));
  }, [objectManager]);

  const getStatistics = useCallback(() => {
    return objectManager.getObjectStatistics();
  }, [objectManager]);

  return {
    createObject,
    destroyObject,
    getStatistics,
    objects: Array.from(objects.keys())
  };
}

// Usage in React Component
function CustomObjectDashboard({ app }: { app: any }) {
  const { createObject, destroyObject, getStatistics, objects } = useCustomObjectManager(app);

  const handleCreateKPI = async () => {
    await createObject('sales-kpi', {
      qInfo: { qType: 'custom-kpi' },
      qHyperCubeDef: {
        qMeasures: [{ qDef: { qDef: 'Sum(Sales)' } }],
        qInitialDataFetch: [{ qLeft: 0, qTop: 0, qWidth: 1, qHeight: 1 }]
      }
    });
  };

  return (
    <div>
      <button onClick={handleCreateKPI}>Create Sales KPI</button>
      <div>Active Objects: {objects.length}</div>
      <div>Statistics: {JSON.stringify(getStatistics(), null, 2)}</div>
    </div>
  );
}

🎨 Custom Objects Best Practices

Object Lifecycle: Always clean up session objects when no longer needed
Performance: Limit the number of concurrent session objects
Error Handling: Implement robust error handling for object creation and updates
Data Binding: Use reactive patterns to update visualizations when data changes
Reusability: Create template objects that can be easily configured and reused
User Experience: Provide loading states and error feedback for custom visualizations
Accessibility: Ensure custom visualizations are accessible and keyboard navigable
Testing: Test custom objects with various data sets and edge cases