Portfolio de Inversiones con Inteligencia Artificial.


Google Sheets y Apps Script + Google AI Studio

¿Te pierdes entre filas y columnas intentando calcular tu rentabilidad real?

Con el portfolio de inversiones que vamos a desarrollar con Google Sheets y Apps Script vas a poder analizar todas las acciones de tus compañías de un solo vistazo.

Gracias a la Inteligencia Artificial, dentro de Google AI Studio tenemos herramientas espectaculares, como Gemini Pro, para hacerlo sin ser expertos en finanzas ni en programación.

Simplemente con los Prompts que comparto más abajo contigo, podrás realizar este estupendo Dashboard del Portfolio de Inversiones con Inteligencia Artificial.

Portfolio de Inversión - Dashboard

Al final, tendrás un panel de control interactivo y visual que se actualiza con un clic, dándote una visión clara y completa de tu cartera.

Los Ingredientes para tu Dashboard Inteligente

La belleza de este proyecto es su simplicidad y accesibilidad. Solo necesitas una cuenta de Google y tres componentes clave que trabajarán en equipo para dar vida a nuestro panel de control.

  • El primero es Google Sheets, que actuará como nuestra base de datos. Aquí es donde registrarás todas tus operaciones de compra de acciones.
  • El segundo es Google Apps Script, el motor que se esconde detrás de la hoja de cálculo. Es un lenguaje de programación basado en JavaScript que leerá nuestros datos, hará todos los cálculos complejos por nosotros y preparará la información para mostrarla.
  • Finalmente, el toque de magia lo pone la Inteligencia Artificial (a través de Google AI Studio), que usamos como un experto consultor y desarrollador. La IA nos puede ayudar a analizar la estructura de datos, sugerir los mejores KPIs y gráficos, y generar el código necesario.

Libros de inversión recomendados

Y si estás interesado en la inversión, te recomiendo estos dos libros, que yo ya he leído, para perderle el miedo a la inversión, y quien sabe, consigas la independencia financiera…

Paso 1: Tus Datos en Google Sheets

Todo gran análisis empieza con datos bien estructurados. En nuestro proyecto, creamos una hoja de cálculo llamada «INVERSIONES« y, dentro de ella, una hoja específica llamada «portfolio».

La clave del éxito aquí es la organización. Definimos columnas claras y concisas que nos permitirán realizar cualquier cálculo que necesitemos más adelante.

Nuestra estructura incluye columnas esenciales como ‘TICKER’, ‘BOLSA’, ‘MONEDA’, ‘NOMBRE’, ‘SECTOR’, ‘OPER’ (tipo de operación), ‘FECHA’, ‘PCOMPRA’ (precio de compra), ‘ACCIONES’ y ‘PACTUAL’ (precio actual). También añadimos columnas calculadas como ‘P/G’ (Pérdida/Ganancia) y ‘RENTABILIDAD’.

Hoja Inversiones Portfolio

Tener esta base de datos ordenada es el primer paso fundamental para crear un portfolio de inversiones con Inteligencia Artificial, ya que la calidad de las respuestas de la IA y de nuestro panel depende directamente de la calidad de los datos que le proporcionamos.

Puedes realizar una copia de la hoja de cálculo en el siguiente botón.

Para que puedas trabajar como en el tutorial, puedes realizar una copia de la hoja de cálculo en el siguiente botón.

Paso 2: El Cerebro del Proyecto, Google Apps Script

Aquí es donde ocurre la magia de la automatización. Vinculado a nuestra hoja de cálculo, creamos un script (en el archivo Code.gs) que actúa como el cerebro de nuestro panel. Su misión es simple pero poderosa: conectarse a la hoja «portfolio», leer cada una de las filas y realizar todos los cálculos necesarios en cuestión de milisegundos.

Le pedimos a la IA que nos generara una función principal, getDashboardData(), que se convertiría en el corazón de la operación.

Esta función no solo calcula las cifras clave (KPIs) que veremos en la parte superior del panel, como la inversión total o la ganancia/pérdida acumulada, sino que también realiza un trabajo de agrupación de datos fundamental.

Agrupa las inversiones por sector, por país, por moneda y por ticker. De esta forma, prepara y sirve toda la información ya «cocinada» para que nuestra interfaz visual solo tenga que preocuparse de mostrarla de forma bonita.

Google Apps Script - Portfolio de inversiones

Este es el poder de la automatización: transformar datos brutos en información valiosa.

Paso 3: Dando Vida al Panel, La Interfaz Visual

Con los datos ya procesados por nuestro script, el siguiente paso fue construir la parte visible del iceberg: el panel de control (nuestro archivo index.html).

Aplicación Web creada con Google AI Studio
Aplicación Web creada con Google AI Studio

Gracias a la Inteligencia Artificial de Google Studio no necesitas ningún conocimiento previo de programación y podrás diseñar una interfaz limpia y funcional utilizando HTML para la estructura, CSS para el estilo y JavaScript para la interactividad, incluyendo la fantástica librería de gráficos ApexCharts.js.

El diseño se divide en dos partes.

  • Primero, una fila superior con seis tarjetas de KPIs que ofrecen una visión instantánea del estado de la cartera: «Inversión Total», «Valor Actual», «Ganancia/Pérdida», «Rentabilidad (%)», «Mayor Posición» y «Mayor Rendimiento». Estas tarjetas son lo primero que ves y te dan la información más crítica de un solo vistazo.
  • Segundo, creamos una estructura de tres columnas para organizar siete gráficos diferentes, cada uno diseñado para responder a una pregunta específica sobre nuestras inversiones: ¿cómo se compone mi cartera?, ¿qué sectores son más rentables?, ¿a qué monedas estoy más expuesto?
Portfolio de Inversión - Dashboard
Dashboard creado con Inteligencia Artificial

Te comparto el dashboard para que puedas ver en tiempo real, sobre la hoja INVERSIONES, como calcula todos los datos de las acciones cotizadas.

Conclusión

Hemos pasado de una hoja de cálculo estática a un portfolio de inversiones creado con Inteligencia Artificial dinámico e interactivo.

El resultado es una herramienta profesional que nos permite entender nuestra estrategia de inversión, identificar riesgos y oportunidades, y, en definitiva, tomar mejores decisiones financieras.

Todo ello, construido con herramientas gratuitas y con la ayuda experta de la Gemini Pro que ha realizado toda la programación. Además de ser un asistente que nos puede cambiar cualquier cosa en cualquier momento.

Mis libros recomendados sobre inversión.


Vídeo tutorial: Portfolio de Inversiones AI con Sheets 🔥【 FÁCIL Y RÁPIDO】

Puedes seguir paso a paso todo lo que te he contado en este vídeo tutorial de forma sencilla y en unos minutos

Prompts para el Dashboard – Portfolio de Inversión.

Comparto contigo los Prompts que he utilizado en Google Studio AI para crear el Dashboard del Portfolio de Inversión.

PROMPT 1 – GENERAL

PROMPT 2 – Detalle de los gráficos del Dashboard

Espera, si te ha sido útil todo lo visto aquí.. Te pido un favor. Ayúdame a compartirlo en las redes sociales!

Código generado por Gemini Pro 2.5

A continuación te presento los códigos que ha generado Gemini para crear el portafolio de acciones. Puedes copiarlo y pegarlo en Apps Script y si mantienes la estructura de la hoja de datos, podrás obtener el portafolio creado:

Código.gs

/**
 * @OnlyCurrentDoc
 * Se encarga de servir la aplicación web.
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('Panel de Control de Inversiones')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * Obtiene, procesa y retorna los datos del portfolio para el dashboard.
 * @returns {object} Un objeto con todas las métricas y datos de gráficos.
 */
function getDashboardData() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName('portfolio');
    if (!sheet) {
      throw new Error('No se encontró la hoja "portfolio". Revisa que el nombre sea exacto.');
    }
    const values = sheet.getDataRange().getValues();
    const headers = values[0];
    const data = values.slice(1);

    const colMap = headers.reduce((map, header, i) => {
      map[header.trim().toUpperCase()] = i; return map;
    }, {});
    
    const requiredCols = ['TICKER', 'NOMBRE', 'OPER', 'PCOMPRA', 'ACCIONES', 'PACTUAL', 'P/G', 'PAIS', 'SECTOR', 'MONEDA'];
    for (const col of requiredCols) {
        if (colMap[col] === undefined) {
             if (col === 'PAIS' && colMap['PAIS'] === undefined) { colMap['PAIS'] = 12; } 
             else { throw new Error(`La columna requerida "${col}" no se encuentra en la hoja.`); }
        }
    }

    let inversionTotal = 0;
    let valorActualTotal = 0;
    const portfolioByCompany = {};
    const portfolioByTicker = {};
    const portfolioByCountry = {};
    const portfolioBySector = {};
    const portfolioBySectorPerformance = {};
    const portfolioByCurrency = {}; // <-- NUEVO para Gráfico 6

    const parseCurrency = (value) => {
      if (typeof value === 'number') return value;
      if (typeof value !== 'string' || value.trim() === '') return 0;
      const cleanedValue = value.replace(/€|'| /g, '').replace(/\./g, '').replace(',', '.');
      return parseFloat(cleanedValue) || 0;
    };

    data.forEach(row => {
      const ticker = row[colMap['TICKER']];
      const nombre = row[colMap['NOMBRE']];
      const sector = row[colMap['SECTOR']];
      const moneda = row[colMap['MONEDA']] || 'N/A';
      const oper = row[colMap['OPER']];
      const pais = row[colMap['PAIS']] || 'Desconocido';
      const pCompra = parseCurrency(row[colMap['PCOMPRA']]);
      const acciones = parseFloat(row[colMap['ACCIONES']]) || 0;
      const pActual = parseCurrency(row[colMap['PACTUAL']]);
      const pg = parseCurrency(row[colMap['P/G']]);

      if (!ticker || !nombre || acciones === 0) return;
      
      const valorActualFila = pActual * acciones;
      
      if (oper.toLowerCase() === 'compra' && pCompra > 0) inversionTotal += pCompra * acciones;
      valorActualTotal += valorActualFila;
      if (!portfolioByCompany[nombre]) portfolioByCompany[nombre] = { valorActual: 0, pgTotal: 0 };
      portfolioByCompany[nombre].valorActual += valorActualFila;
      portfolioByCompany[nombre].pgTotal += pg;

      if (!portfolioByTicker[ticker]) portfolioByTicker[ticker] = { valorActual: 0, invertido: 0, pgTotal: 0 };
      portfolioByTicker[ticker].valorActual += valorActualFila;
      portfolioByTicker[ticker].pgTotal += pg;
      if (oper.toLowerCase() === 'compra' && pCompra > 0) portfolioByTicker[ticker].invertido += pCompra * acciones;
      
      if (!portfolioByCountry[pais]) portfolioByCountry[pais] = 0;
      portfolioByCountry[pais] += valorActualFila;
      
      if (!portfolioBySector[sector]) portfolioBySector[sector] = 0;
      portfolioBySector[sector] += valorActualFila;

      if (!portfolioBySectorPerformance[sector]) portfolioBySectorPerformance[sector] = 0;
      portfolioBySectorPerformance[sector] += pg;
      
      // <-- NUEVA AGREGACIÓN POR MONEDA -->
      if (!portfolioByCurrency[moneda]) portfolioByCurrency[moneda] = 0;
      portfolioByCurrency[moneda] += valorActualFila;
    });

    const kpis = (function() { /* ... Lógica de KPIs sin cambios ... */
      const gananciaPerdida = valorActualTotal - inversionTotal;
      const rentabilidad = inversionTotal !== 0 ? (gananciaPerdida / inversionTotal) * 100 : 0;
      let mayorPosicionNombre = 'N/A', mayorPosicionValor = 0, mayorRendimientoNombre = 'N/A', mayorRendimientoValor = -Infinity;
      for (const nombre in portfolioByCompany) {
        if (portfolioByCompany[nombre].valorActual > mayorPosicionValor) { mayorPosicionValor = portfolioByCompany[nombre].valorActual; mayorPosicionNombre = nombre; }
        if (portfolioByCompany[nombre].pgTotal > mayorRendimientoValor) { mayorRendimientoValor = portfolioByCompany[nombre].pgTotal; mayorRendimientoNombre = nombre; }
      }
      return { inversionTotal, valorActualTotal, gananciaPerdida, rentabilidad, mayorPosicion: { nombre: mayorPosicionNombre, valor: mayorPosicionValor }, mayorRendimiento: { nombre: mayorRendimientoNombre, valor: mayorRendimientoValor } };
    })();
    
    const charts = (function() {
        const tickers = Object.keys(portfolioByTicker);
        const countries = Object.keys(portfolioByCountry);
        const sectors = Object.keys(portfolioBySector);
        const performanceSectors = Object.keys(portfolioBySectorPerformance);
        const currencies = Object.keys(portfolioByCurrency); // <-- NUEVO

        const chart1Data = { labels: tickers, series: tickers.map(t => portfolioByTicker[t].valorActual) };
        const chart2Data = { categories: countries, series: [{ name: 'Valor Actual', data: countries.map(c => portfolioByCountry[c]) }] };
        const chart3Data = { categories: performanceSectors, series: [{ name: 'P/G', data: performanceSectors.map(s => portfolioBySectorPerformance[s]) }] };
        const chart4Data = { categories: tickers, series: [{ name: 'P/G', data: tickers.map(t => portfolioByTicker[t].pgTotal) }] };
        const chart5Data = { labels: sectors, series: sectors.map(s => portfolioBySector[s]) };
        
        // <-- LÓGICA MODIFICADA PARA GRÁFICO 6 -->
        const chart6Data = { labels: currencies, series: currencies.map(c => portfolioByCurrency[c]) };

        const chart7Data = {
            categories: tickers,
            series: [ { name: 'Invertido', data: tickers.map(t => portfolioByTicker[t].invertido) }, { name: 'Actual', data: tickers.map(t => portfolioByTicker[t].valorActual) } ]
        };
        return { chart1Data, chart2Data, chart3Data, chart4Data, chart5Data, chart6Data, chart7Data };
    })();

    return { kpis, charts, error: null };
  } catch (e) {
    Logger.log(e.toString());
    return { error: e.toString() };
  }
}

index.html

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <title>Panel de Control de Inversiones</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
  <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>

  <style>
    :root { /* ... Estilos sin cambios ... */
      --main-color: #021640; --color-1: #0066CC; --color-2: #0099CC; --color-3: #4572C4;
      --color-4: #558ED5; --color-5: #1F4E79;
    }
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f4f7f9; margin: 0; padding: 24px; }
    .main-header { background-color: var(--main-color); color: #ffffff; padding: 20px; text-align: center; border-radius: 8px; margin-bottom: 24px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
    .main-header h1 { margin: 0; font-size: 2.2rem; }
    .main-header h2 { margin: 5px 0 0; font-weight: 300; font-size: 1.2rem; opacity: 0.9; }
    .cards-container { display: flex; flex-wrap: wrap; gap: 24px; justify-content: center; margin-bottom: 24px; }
    .kpi-card { background-color: #ffffff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); padding: 20px; width: 290px; height: 100px; display: flex; flex-direction: column; justify-content: space-between; color: var(--main-color); transition: transform 0.2s, box-shadow 0.2s; border-left: 5px solid var(--main-color); }
    .kpi-card:hover { transform: translateY(-5px); box-shadow: 0 8px 16px rgba(0,0,0,0.1); }
    .card-header { display: flex; justify-content: space-between; align-items: flex-start; font-size: 0.9rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
    .card-header .icon { font-size: 1.5rem; opacity: 0.7; }
    .card-value { font-size: 2rem; font-weight: 700; }
    .card-value-dual { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
    .main-text { font-size: 1.5rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
    .sub-text { font-size: 1.1rem; font-weight: 500; }
    
    /* --- ▼▼▼ REGLA CSS MODIFICADA ▼▼▼ --- */
    .charts-grid { 
      display: grid; 
      /* Proporciones: 37.5% | 25% | 37.5% */
      grid-template-columns: 3fr 2fr 3fr;
      gap: 24px; 
    }
    /* --- ▲▲▲ FIN REGLA MODIFICADA ▲▲▲ --- */

    .chart-column { display: flex; flex-direction: column; gap: 24px; }
    .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); display: flex; flex-direction: column; }
    .chart-card h3 { margin: 0 0 15px 0; color: var(--main-color); font-size: 1.1rem; font-weight: 600; text-transform: uppercase; }
    .chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
    #chart-4-card { grid-row: 1 / 3; }
    #loading-state { text-align: center; padding: 40px; font-size: 1.2rem; color: #555; }
    .hidden { display: none; }
    .positive { color: #28a745 !important; } .positive-border { border-left-color: #28a745; }
    .negative { color: #dc3545 !important; } .negative-border { border-left-color: #dc3545; }
    .apexcharts-tooltip { color: #333; }

    /* Para mantener la responsividad en pantallas pequeñas */
    @media (max-width: 1200px) {
      .charts-grid {
        grid-template-columns: 1fr 1fr; /* Pasa a 2 columnas en tablets */
      }
    }
    @media (max-width: 768px) {
      .charts-grid {
        grid-template-columns: 1fr; /* Pasa a 1 columna en móviles */
      }
    }
  </style>
</head>
<body>
  <header class="main-header">
      <h1>Portfolio de Inversión</h1>
  </header>

  <div id="loading-state">Cargando datos del portfolio...</div>

  <main id="dashboard-content" class="hidden">
    <section class="cards-container">
        <!-- KPIs se mantienen igual -->
        <div class="kpi-card"><div class="card-header"><span>Inversión Total</span><i class="fas fa-wallet icon"></i></div><div id="inversionTotal" class="card-value">--</div></div><div class="kpi-card"><div class="card-header"><span>Valor Actual</span><i class="fas fa-chart-line icon"></i></div><div id="valorActual" class="card-value">--</div></div><div id="gananciaPerdidaCard" class="kpi-card"><div class="card-header"><span>Ganancia/Pérdida</span><i class="fas fa-arrow-trend-up icon"></i></div><div id="gananciaPerdida" class="card-value">--</div></div><div id="rentabilidadCard" class="kpi-card"><div class="card-header"><span>Rentabilidad (%)</span><i class="fas fa-percentage icon"></i></div><div id="rentabilidad" class="card-value">--</div></div><div class="kpi-card"><div class="card-header"><span>Mayor Posición</span><i class="fas fa-trophy icon"></i></div><div class="card-value-dual"><span id="mayorPosicionNombre" class="main-text">--</span><span id="mayorPosicionValor" class="sub-text">--</span></div></div><div class="kpi-card"><div class="card-header"><span>Mayor Rendimiento</span><i class="fas fa-star icon"></i></div><div class="card-value-dual"><span id="mayorRendimientoNombre" class="main-text">--</span><span id="mayorRendimientoValor" class="sub-text">--</span></div></div>
    </section>

    <!-- La estructura HTML no cambia -->
    <section class="charts-grid">
      <!-- Columna 1 (ahora 37.5% de ancho) -->
      <div class="chart-column">
        <div class="chart-card" style="min-height: 300px;"><h3>COMPOSICIÓN DE CARTERA</h3><div id="chart-container-1"></div></div>
        <div class="chart-row">
          <div class="chart-card" style="min-height: 350px;"><h3 style="text-align: center;">CARTERA/PAÍS</h3><div id="chart-container-2"></div></div>
          <div class="chart-card" style="min-height: 350px;"><h3>CARTERA/MONEDA</h3><div id="chart-container-6"></div></div>
        </div>
      </div>

      <!-- Columna 2 (ahora 25% de ancho) -->
      <div class="chart-column"><div class="chart-card" id="chart-4-card" style="min-height: 674px;"><h3>PÉRDIDAS/GANANCIAS</h3><div id="chart-container-4"></div></div></div>

      <!-- Columna 3 (ahora 37.5% de ancho) -->
      <div class="chart-column">
        <div class="chart-row">
          <div class="chart-card" style="min-height: 350px;"><h3>CARTERA/SECTOR</h3><div id="chart-container-5"></div></div>
          <div class="chart-card" style="min-height: 350px;"><h3 style="text-align: center;">RENDIMIENTO POR SECTOR</h3><div id="chart-container-3"></div></div>
        </div>
        <div class="chart-card" style="min-height: 300px;"><h3>Invertido / Actual</h3><div id="chart-container-7"></div></div>
      </div>
    </section>
  </main>

  <script>
    // --- LA SECCIÓN DE JAVASCRIPT NO TIENE CAMBIOS ---
    const chartColors = ['#021640', '#0066CC', '#0099CC', '#4572C4', '#558ED5', '#1F4E79', '#8497B0', '#A5A5A5'];
    const FONT_COLOR = '#021640';
    const FONT_FAMILY = 'inherit';

    const formatCurrency = (val) => typeof val !== 'number' ? '--' : new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(val);
    const formatPercentage = (val) => typeof val !== 'number' ? '--' : val.toFixed(2).replace('.', ',') + ' %';
    const formatK = (val) => (val / 1000).toFixed(1).replace('.', ',') + 'K';

    const renderChart = (selector, options) => {
        const container = document.querySelector(selector);
        if (container) { container.innerHTML = ""; new ApexCharts(container, options).render(); }
    };
    
    function renderChart1(d) {
        renderChart("#chart-container-1", { series: d.series, labels: d.labels, colors: chartColors, chart: { type: 'pie', height: 300, fontFamily: FONT_FAMILY }, legend: { position: 'bottom', labels: { colors: [FONT_COLOR] } }, tooltip: { y: { formatter: (val) => formatCurrency(val) } }, dataLabels: { formatter: (val) => val.toFixed(1) + '%' } });
    }
    function renderChart2(d) {
        renderChart("#chart-container-2", { series: d.series, chart: { type: 'bar', height: 350, fontFamily: FONT_FAMILY, toolbar: { show: false } }, colors: [chartColors[3]], plotOptions: { bar: { borderRadius: 4, horizontal: false, } }, dataLabels: { enabled: false }, xaxis: { categories: d.categories, labels: { style: { colors: FONT_COLOR } } }, yaxis: { labels: { formatter: formatK, style: { colors: [FONT_COLOR] } } }, tooltip: { y: { formatter: (val) => formatCurrency(val) } } });
    }
    function renderChart3(d) {
        renderChart("#chart-container-3", { series: d.series, chart: { type: 'bar', height: 350, fontFamily: FONT_FAMILY }, plotOptions: { bar: { horizontal: true, colors: { ranges: [{ from: 0, to: 1000000, color: '#28a745' }, { from: -1000000, to: 0, color: '#dc3545' }] } } }, dataLabels: { enabled: false }, xaxis: { categories: d.categories, labels: { formatter: formatK, style: { colors: FONT_COLOR } } }, yaxis: { labels: { style: { colors: FONT_COLOR, fontSize: '11px' } } }, tooltip: { y: { formatter: (val) => formatCurrency(val) } } });
    }
    function renderChart4(d) {
        renderChart("#chart-container-4", { series: d.series, chart: { type: 'bar', height: 650, fontFamily: FONT_FAMILY }, plotOptions: { bar: { horizontal: true, barHeight: '80%', colors: { ranges: [{ from: 0, to: 1000000, color: '#28a745' }, { from: -1000000, to: 0, color: '#dc3545' }] } } }, dataLabels: { enabled: true, formatter: formatK, style: { colors: ['#fff'] } }, xaxis: { categories: d.categories, labels: { formatter: formatK, style: { colors: FONT_COLOR } } }, yaxis: { labels: { style: { colors: FONT_COLOR } } }, grid: { xaxis: { lines: { show: true } } }, tooltip: { y: { formatter: (val) => formatCurrency(val) } } });
    }
    function renderChart5(d) {
        renderChart("#chart-container-5", { series: d.series, chart: { type: 'donut', height: 350, fontFamily: FONT_FAMILY }, plotOptions: { pie: { donut: { size: '65%' } } }, labels: d.labels, colors: chartColors, legend: { position: 'bottom', labels: { colors: [FONT_COLOR] } }, tooltip: { y: { formatter: (val) => formatCurrency(val), title: { formatter: (s, o) => o.w.globals.labels[o.seriesIndex] } } }, dataLabels: { formatter: (val) => val.toFixed(1) + '%' } });
    }
    function renderChart6(d) {
      renderChart("#chart-container-6", {
        series: d.series,
        chart: { type: 'donut', height: 350, fontFamily: FONT_FAMILY },
        labels: d.labels,
        colors: chartColors,
        legend: { position: 'bottom', labels: { colors: [FONT_COLOR] } },
        tooltip: { y: { formatter: (val) => formatCurrency(val) } },
        dataLabels: { formatter: (val) => val.toFixed(1) + '%' }
      });
    }
    function renderChart7(d) {
        renderChart("#chart-container-7", { series: d.series, chart: { type: 'bar', height: 300, fontFamily: FONT_FAMILY }, colors: [chartColors[1], chartColors[3]], plotOptions: { bar: { horizontal: false, columnWidth: '55%', borderRadius: 4 } }, dataLabels: { enabled: false }, xaxis: { categories: d.categories, labels: { style: { colors: FONT_COLOR } } }, yaxis: { labels: { formatter: formatK, style: { colors: [FONT_COLOR] } } }, legend: { labels: { colors: [FONT_COLOR] } }, tooltip: { y: { formatter: (val) => formatCurrency(val) } }, });
    }

    function updateDashboard(data) {
      document.getElementById('loading-state').classList.add('hidden');
      document.getElementById('dashboard-content').classList.remove('hidden');
      if (data.error) { handleError(data.error); return; }
      
      const { kpis, charts } = data;
      // ... (código de actualización de KPIs sin cambios)
      document.getElementById('inversionTotal').textContent = formatCurrency(kpis.inversionTotal); document.getElementById('valorActual').textContent = formatCurrency(kpis.valorActualTotal); document.getElementById('gananciaPerdida').textContent = formatCurrency(kpis.gananciaPerdida); document.getElementById('rentabilidad').textContent = formatPercentage(kpis.rentabilidad); document.getElementById('mayorPosicionNombre').textContent = kpis.mayorPosicion.nombre; document.getElementById('mayorPosicionValor').textContent = formatCurrency(kpis.mayorPosicion.valor); document.getElementById('mayorRendimientoNombre').textContent = kpis.mayorRendimiento.nombre; document.getElementById('mayorRendimientoValor').textContent = formatCurrency(kpis.mayorRendimiento.valor); const gpCard = document.getElementById('gananciaPerdidaCard'); gpCard.classList.toggle('positive-border', kpis.gananciaPerdida >= 0); gpCard.classList.toggle('negative-border', kpis.gananciaPerdida < 0); document.getElementById('gananciaPerdida').classList.toggle('positive', kpis.gananciaPerdida >= 0); document.getElementById('gananciaPerdida').classList.toggle('negative', kpis.gananciaPerdida < 0); const rCard = document.getElementById('rentabilidadCard'); rCard.classList.toggle('positive-border', kpis.rentabilidad >= 0); rCard.classList.toggle('negative-border', kpis.rentabilidad < 0); document.getElementById('rentabilidad').classList.toggle('positive', kpis.rentabilidad >= 0); document.getElementById('rentabilidad').classList.toggle('negative', kpis.rentabilidad < 0); const mrvElem = document.getElementById('mayorRendimientoValor'); mrvElem.classList.toggle('positive', kpis.mayorRendimiento.valor >= 0); mrvElem.classList.toggle('negative', kpis.mayorRendimiento.valor < 0);

      renderChart1(charts.chart1Data);
      renderChart2(charts.chart2Data);
      renderChart3(charts.chart3Data);
      renderChart4(charts.chart4Data);
      renderChart5(charts.chart5Data);
      renderChart6(charts.chart6Data);
      renderChart7(charts.chart7Data);
    }

    function handleError(error) {
        const loadingElem = document.getElementById('loading-state');
        loadingElem.textContent = `Error al cargar los datos: ${error.toString()}`;
        loadingElem.style.color = '#dc3545';
        console.error('Error al obtener los datos:', error);
    }

    window.addEventListener('load', () => google.script.run.withSuccessHandler(updateDashboard).withFailureHandler(handleError).getDashboardData());
  </script>
</body>
</html>

También te puede interesar…