Your IP : 216.73.216.79


Current Path : /var/www/html/reddsis/node_modules/apexcharts/src/ssr/
Upload File :
Current File : /var/www/html/reddsis/node_modules/apexcharts/src/ssr/SSRRenderer.js

// @ts-check
import ApexCharts from '../apexcharts.js'
import { BrowserAPIs } from './BrowserAPIs.js'
import { Environment } from '../utils/Environment.js'

/**
 * SSRRenderer - Server-Side Rendering API for ApexCharts
 *
 * Provides methods to render charts to SVG strings or hydration-ready HTML
 * for use in Node.js, Next.js, Nuxt, SvelteKit, and other SSR frameworks.
 */
export class SSRRenderer {
  /**
   * Render chart to SVG string for server-side rendering
   *
   * @param {Record<string, any>} options - Chart configuration (same as ApexCharts constructor)
   * @param {{ width?: number, height?: number, scale?: number }} [ssrOptions] - SSR-specific options
   * @returns {Promise<string>} SVG string
   *
   * @example
   * const svgString = await SSRRenderer.renderToString({
   *   series: [{ data: [30, 40, 35] }],
   *   chart: { type: 'bar' }
   * }, {
   *   width: 500,
   *   height: 300
   * });
   */
  static async renderToString(options, ssrOptions = {}) {
    // Initialize BrowserAPIs shim if in SSR
    if (Environment.isSSR()) {
      BrowserAPIs.init()
    }

    const { width = 400, height = 300, scale = 1 } = ssrOptions

    // Create virtual element context for SSR
    const virtualEl = this._createVirtualElement(width, height)

    // Merge config with SSR overrides
    const ssrConfig = {
      ...options,
      chart: {
        ...options.chart,
        width,
        height,
        // Disable interactive features for SSR
        toolbar: { show: false },
        animations: { enabled: false },
      },
    }

    // Create chart instance
    const chart = new ApexCharts(
      /** @type {HTMLElement} */ (virtualEl),
      ssrConfig,
    )

    try {
      // Render the chart
      await chart.render()

      // Extract SVG string
      // The chart's SVG is in gl.dom.Paper.node
      const svgString = this._extractSVGString(chart, scale)

      // Clean up
      chart.destroy()

      return svgString
    } catch (error) {
      // Ensure cleanup on error
      chart.destroy()
      throw new Error(
        `SSR rendering failed: ${/** @type {any} */ (error).message}`,
      )
    }
  }

  /**
   * Generate hydration-ready HTML with embedded configuration
   *
   * @param {Record<string, any>} options - Chart configuration
   * @param {{ width?: number, height?: number, scale?: number, className?: string }} [ssrOptions] - SSR-specific options
   * @returns {Promise<string>} HTML string with SVG and hydration data
   *
   * @example
   * const html = await SSRRenderer.renderToHTML({
   *   series: [{ data: [30, 40, 35] }],
   *   chart: { type: 'bar' }
   * }, {
   *   width: 500,
   *   height: 300
   * });
   */
  static async renderToHTML(options, ssrOptions = {}) {
    const { className = '' } = ssrOptions

    // Render to SVG string
    const svgString = await this.renderToString(options, ssrOptions)

    // Encode configuration for client-side hydration
    const dataConfig = this._encodeConfig(options)

    // Generate hydration-ready HTML
    const wrapperClass = `apexcharts-ssr-wrapper${className ? ' ' + className : ''}`

    return `<div class="${wrapperClass}" data-apexcharts-hydrate data-apexcharts-config="${dataConfig}">
${svgString}
</div>`
  }

  /**
   * Create a virtual DOM element for SSR rendering
   * @private
   * @param {number} width
   * @param {number} height
   */
  static _createVirtualElement(width, height) {
    if (Environment.isBrowser()) {
      // In browser, create real element
      const el = document.createElement('div')
      el.style.width = `${width}px`
      el.style.height = `${height}px`
      return el
    }

    // In SSR, create virtual element
    return {
      _ssrWidth: width,
      _ssrHeight: height,
      _ssrMode: true,
      nodeType: 1,
      nodeName: 'DIV',
      children: /** @type {any[]} */ ([]),
      style: {},
      classList: {
        add: () => {},
        remove: () => {},
        contains: () => false,
      },
      /**
       * @param {any} child
       */
      appendChild(child) {
        this.children.push(child)
      },
      /**
       * @param {any} child
       */
      removeChild(child) {
        const index = this.children.indexOf(child)
        if (index > -1) this.children.splice(index, 1)
      },
      querySelector() {
        return null
      },
      querySelectorAll() {
        return []
      },
      getElementsByClassName() {
        return []
      },
      getAttribute() {
        return null
      },
      setAttribute() {},
      removeAttribute() {},
      hasAttribute() {
        return false
      },
      getBoundingClientRect() {
        return {
          width: this._ssrWidth,
          height: this._ssrHeight,
          top: 0,
          left: 0,
          right: this._ssrWidth,
          bottom: this._ssrHeight,
          x: 0,
          y: 0,
        }
      },
      get parentNode() {
        return null
      },
      get isConnected() {
        return true
      },
      getRootNode() {
        return this
      },
    }
  }

  /**
   * Extract SVG string from rendered chart
   * @private
   * @param {any} chart
   */
  static _extractSVGString(chart, scale = 1) {
    const w = chart.w

    if (!w || !w.dom || !w.dom.Paper) {
      throw new Error('Chart not properly initialized')
    }

    const svgNode = w.dom.Paper.node

    // If we have a real SVG element (browser), serialize it
    if (Environment.isBrowser() && svgNode instanceof SVGElement) {
      const serializer = new XMLSerializer()
      let svgString = serializer.serializeToString(svgNode)

      // Apply scale if needed
      if (scale !== 1) {
        svgString = this._applyScale(svgString, scale)
      }

      return svgString
    }

    // In SSR, use the toString() method from SSRElement
    if (svgNode && typeof svgNode.toString === 'function') {
      let svgString = svgNode.toString()

      // Apply scale if needed
      if (scale !== 1) {
        svgString = this._applyScale(svgString, scale)
      }

      return svgString
    }

    throw new Error('Unable to extract SVG string from chart')
  }

  /**
   * Apply scale transformation to SVG string
   * @private
   * @param {string} svgString
   * @param {number} scale
   */
  static _applyScale(svgString, scale) {
    // Parse width and height from SVG
    const widthMatch = svgString.match(/width="([^"]+)"/)
    const heightMatch = svgString.match(/height="([^"]+)"/)

    if (widthMatch && heightMatch) {
      const width = parseFloat(widthMatch[1])
      const height = parseFloat(heightMatch[1])
      const scaledWidth = width * scale
      const scaledHeight = height * scale

      // Replace width and height attributes
      svgString = svgString
        .replace(/width="[^"]+"/, `width="${scaledWidth}"`)
        .replace(/height="[^"]+"/, `height="${scaledHeight}"`)
    }

    return svgString
  }

  /**
   * Encode configuration for client-side hydration
   * @private
   * @param {Record<string, any>} config
   */
  static _encodeConfig(config) {
    try {
      const json = JSON.stringify(config)

      // Use Buffer in Node.js, btoa in browser
      if (typeof Buffer !== 'undefined') {
        return Buffer.from(json).toString('base64')
      } else if (typeof btoa !== 'undefined') {
        return btoa(json)
      }

      // Fallback: URL-safe JSON encoding
      return encodeURIComponent(json)
    } catch (error) {
      throw new Error(
        `Failed to encode config: ${/** @type {any} */ (error).message}`,
      )
    }
  }

  /**
   * Decode configuration from hydration data
   * @private
   * @param {string} encodedConfig
   */
  static _decodeConfig(encodedConfig) {
    try {
      let json

      // Try Buffer first (Node.js)
      if (typeof Buffer !== 'undefined') {
        json = Buffer.from(encodedConfig, 'base64').toString('utf-8')
      } else if (typeof atob !== 'undefined') {
        json = atob(encodedConfig)
      } else {
        // Fallback: URL-safe JSON decoding
        json = decodeURIComponent(encodedConfig)
      }

      return JSON.parse(json)
    } catch (error) {
      throw new Error(
        `Failed to decode config: ${/** @type {any} */ (error).message}`,
      )
    }
  }
}