Gráficas Python

Copia y pega el siguiente código en tu proyecto de Python para generar una gráfica SVG con estilo personalizado.

Gráfica de barras agrupadas y apiladas
Ejemplo gráfico agrupadas y apiladas

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))

def agrupadasyapiladas(dataframes):
    """
    Genera una gráfica de barras apiladas agrupadas para múltiples DataFrames.
    Args:
        dataframes (list of pd.DataFrame): Lista de DataFrames con los datos de los grupos de subgrupos apilados.
    """
    font_config = {
        'family': 'Arial',
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 9, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 14, 'weight': 'medium', 'color': '#767676'}
    }

    # Configuración de los grupos y subgrupos
    grupos = dataframes[0].index.tolist()
    subgrupos_list = [df.columns.tolist() for df in dataframes]

    # Obtener colores desde la función colores()
    lista_colores = ["#006157", "#767676", "#671435", "#9B2247", "#9D792A", "#D5B162", "#10302C", "#E6D194", "#018477", "#FF6666", "#00008B", "#854991"]
    colores_por_grupo = []
    color_index = 0
    for subgrupos in subgrupos_list:
        colores_por_grupo.append(lista_colores[color_index:color_index + len(subgrupos)])
        color_index += len(subgrupos)

    # Configuración visual
    factor_ancho = 0.4
    ancho_total = len(grupos) * factor_ancho
    max_subgrupos = max(len(df.columns) for df in dataframes)
    ancho_barra = ancho_total / (len(dataframes) * max_subgrupos)
    desplazamiento = -ancho_total / 2 + ancho_barra / 2

    # Configurar el tamaño de la figura en píxeles
    ancho_px = 1480
    alto_px = 520
    dpi = 100
    ancho_in = ancho_px / dpi
    alto_in = alto_px / dpi

    fig, ax = plt.subplots(figsize=(ancho_in, alto_in), dpi=dpi)
    x_pos = np.arange(len(grupos))

    for df, colores, offset in zip(dataframes, colores_por_grupo, range(len(dataframes))):
        bottom = np.zeros(len(grupos))
        total_grupo = df.sum(axis=1)
        for subgrupo, color in zip(df.columns, colores):
            valores = df[subgrupo]
            ax.bar(
                x_pos + desplazamiento,
                valores,
                width=ancho_barra,
                bottom=bottom,
                color=color,
                label=subgrupo,
            )
            for i, valor in enumerate(valores):
                porcentaje = (valor / total_grupo.iloc[i]) * 100 if total_grupo.iloc[i] != 0 else 0
                ax.text(
                    x_pos[i] + desplazamiento,
                    bottom[i] + valor / 2,
                    f"{int(valor):,} ({porcentaje:.1f}%)",
                    ha='center',
                    va='center',
                    fontsize=font_config['capsula_valor']['size'],
                    fontweight=font_config['capsula_valor']['weight'],
                    color='white' if porcentaje > 10 else 'black'
                )
            bottom += np.array(valores)

        factor_separacion = max(bottom) * 0.05
        for i, total in enumerate(bottom):
            ax.text(
                x_pos[i] + desplazamiento,
                total + factor_separacion,
                f"{int(total):,}",
                ha="center",
                va="bottom",
                fontsize=font_config['capsula_max']['size'],
                fontweight=font_config['capsula_max']['weight'],
                color=colores[1] if len(colores) > 1 else 'black',
                bbox=dict(boxstyle="round,pad=0.35,rounding_size=0.99", fc="white", ec=colores[0], alpha=1.0)
            )

        desplazamiento += ancho_barra

    ax.legend(
        loc='upper center',
        bbox_to_anchor=(0.5, 1.5),
        frameon=False,
        prop={'size': font_config['leyenda']['size'], 'weight': font_config['leyenda']['weight']},
        ncol=max(1, len(dataframes)),
    )

    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(True)
    ax.spines['bottom'].set_linewidth(2)
    ax.grid(axis='y', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{int(x):,}"))
    ax.tick_params(axis='y', labelsize=font_config['eje_y']['size'], labelcolor=font_config['eje_y']['color'])
    ax.yaxis.label.set_size(font_config['eje_y']['size'])
    ax.yaxis.label.set_weight(font_config['eje_y']['weight'])
    ax.yaxis.label.set_color(font_config['eje_y']['color'])
    ax.set_xticks(x_pos)
    ax.set_xticklabels(grupos, fontsize=font_config['eje_x']['size'], fontweight=font_config['eje_x']['weight'], color=font_config['eje_x']['color'])
    ax.set_ylabel('Valores', fontsize=font_config['etiquetas_eje_y']['size'], fontweight=font_config['etiquetas_eje_y']['weight'], color=font_config['etiquetas_eje_y']['color'])
    plt.tight_layout()
    plt.savefig("grafica_agrupadasyapiladas.svg", format="svg", bbox_inches='tight', dpi=dpi)
    plt.show()

# Llamar a la función con una lista de DataFrames
agrupadasyapiladas(dataframes_a_graficar)
Ver carpeta en GitHub
Areaplot
Ejemplo gráfico areaplot

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from scipy.interpolate import make_interp_spline

# VARIABLES A CAMBIAR:
dataframe_a_graficar = data

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))

def areaplot(data):
    """
    Genera un gráfico de área apilada con los datos proporcionados en un DataFrame.
    Parámetros:
    - data: DataFrame que contiene las columnas 'fechas', 'comisiones', 'fiscalias' y 'portal'.
    """
    font_config = {
        'family': 'Arial',
        'titulo': {'size': 16, 'weight': 'bold', 'color': '#10302C'},
        'eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 20, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}
    }

    # Extraer columnas del DataFrame
    fechas = data['fechas']
    comisiones = data['comisiones']
    fiscalias = data['fiscalias']
    portal = data['portal']
    
    # Crear figura
    fig, ax = plt.subplots(figsize=(16, 8))
    n = len(fechas)
    x = np.arange(n) + 0.5

    # Suavizar las curvas visualmente
    x_suave = np.linspace(x.min(), x.max(), 300)
    comisiones_suave = make_interp_spline(x, comisiones)(x_suave)
    fiscalias_suave = make_interp_spline(x, fiscalias)(x_suave)
    portal_suave = make_interp_spline(x, portal)(x_suave)

    # Área apilada
    ax.stackplot(x_suave, comisiones_suave, fiscalias_suave, portal_suave, labels=[
                 'Comisiones', 'Fiscalías y Procuradurías', 'Portal'],
                 colors=['#215F53', '#7570B3', '#C7EAE5'])

    # Eje X
    ax.set_xticks(x)
    ax.set_xticklabels(fechas, rotation=90, fontsize=font_config['eje_x']['size'],
                        fontweight=font_config['eje_x']['weight'], color=font_config['eje_x']['color'])    
    ax.set_xlim(x[0] - 0.5, x[-1] + 0.5)

    # Eje Y
    ax.set_yticks(np.linspace(0, 1, 5))
    ax.set_yticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=font_config['eje_y']['size'],
                        fontweight=font_config['eje_y']['weight'], color=font_config['eje_y']['color'])    
    ax.set_ylabel('Porcentaje', fontsize=font_config['eje_y']['size'],
                  fontweight=font_config['eje_y']['weight'], color=font_config['eje_y']['color'])    

    # Bordes y grid
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(True)
    ax.spines['bottom'].set_linewidth(2)
    ax.grid(axis='y', linestyle='-', color='white', alpha=0.8, linewidth=1.25)
    ax.grid(axis='x', linestyle='-', color='white', alpha=0.3, linewidth=0.75)

    # Coordenadas y valores totales por capa
    x_coords = [2, 8, 14]
    y_coords = [0.15, 0.45, 0.85]
    totales = [round(sum(comisiones), 2), round(sum(fiscalias), 2), round(sum(portal), 2)]
    colores_borde = ['#D9D9D9'] * 3

    for x, y, total, color_borde in zip(x_coords, y_coords, totales, colores_borde):
        ax.text(
            x, y, f"{total:,}",
            ha='center', va='center',
            fontsize=font_config['capsula_valor']['size'],
            fontweight=font_config['capsula_valor']['weight'],
            color=font_config['capsula_valor']['color'],
            bbox=dict(
                boxstyle="round,pad=0.6,rounding_size=0.99",
                facecolor='white',
                edgecolor=color_borde,
                linewidth=1.8
            )
        )

    # Leyenda centrada arriba con tamaño ajustado
    ax.legend(
        loc='upper center',
        bbox_to_anchor=(0.5, 1.12),
        ncol=3,
        frameon=False,
        prop=font_manager.FontProperties(
            size=font_config['leyenda']['size'], 
            weight=font_config['leyenda']['weight']
        )
    )

    plt.tight_layout()
    plt.savefig("areaplot.svg", format="svg", bbox_inches='tight')
    plt.show()

areaplot(dataframe_a_graficar)
Ver carpeta en GitHub
Areaplot 2
Ejemplo gráfico areaplot

from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
import matplotlib.ticker as mticker
from adjustText import adjust_text

# VARIABLES A CAMBIAR
dataframe_a_graficar = df
columna_eje_x = "Fecha"

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def ajusta_etiquetas_apiladas_manual(dataframe, columnas, colores, columna_x, sin_tag=1, etiquetar_max=True, bbox_props=None, fontsize=12, separacion=40):
    """
    Ajusta las etiquetas de los puntos en una gráfica apilada, colocándolas por encima del punto más alto
    y apilándolas verticalmente en orden correcto.

    Args:
        dataframe (pd.DataFrame): DataFrame con los datos.
        columnas (list): Columnas a etiquetar.
        colores (list): Colores para cada etiqueta.
        columna_x (str): Columna para el eje X.
        sin_tag (int): Puntos sin etiqueta entre etiquetas. Default: 2.
        etiquetar_max (bool): Si True, etiqueta el valor máximo de cada columna. Default: True.
        bbox_props (dict): Propiedades del fondo de la etiqueta. Default: None (estilo predeterminado).
        fontsize (int): Tamaño de fuente. Default: 12.
        separacion (int): Separación vertical entre etiquetas (en unidades Y). Default: 40.
    """
    # Calcular la altura acumulada máxima en cada punto X
    acumulado = dataframe[columnas].cumsum(axis=1)
    y_max_global = acumulado.max(axis=1)  # Altura máxima en cada X

    # Diccionario para guardar la posición Y más alta ocupada en cada X
    y_max_por_x = {}

    colores = ["#006157", "#767676", "#671435", "#9B2247", "#9D792A", "#D5B162"]

    for col, color in zip(columnas, colores):
        total_puntos = len(dataframe)
        max_index = dataframe[col].idxmax() if etiquetar_max else None  # Índice del máximo si etiquetar_max=True

        for i, row in dataframe.iterrows():
            x_pos = row[columna_x]  # Posición X actual
            tiene_etiqueta = (
                (total_puntos - i - 1) % (sin_tag + 1) == 0  # Cada 'sin_tag' puntos
                or i == total_puntos - 1  # Siempre etiquetar el último punto
                or (etiquetar_max and i == max_index)  # Etiquetar el máximo si etiquetar_max=True
            )

            if tiene_etiqueta:
                # Altura base: el punto más alto en esta posición X
                y_base = y_max_global[i] - 200  # Desplazar todas las etiquetas hacia abajo (ajusta el valor -200 según sea necesario)
                # Si ya hay una etiqueta en esta X, apilarla encima de la última
                if x_pos in y_max_por_x:
                    y_etiqueta = y_max_por_x[x_pos] + separacion
                else:
                    y_etiqueta = y_base + separacion  # Espacio inicial por encima del punto más alto

                # Actualizar la altura máxima registrada para esta X
                y_max_por_x[x_pos] = y_etiqueta

                # Configurar el fondo de la etiqueta para puntos intermedios
                if i != 1 and i != total_puntos - 1:  # Puntos intermedios
                    bbox_props_intermedio = dict(boxstyle="round,pad=0.3,rounding_size=0.99", fc="white", ec="gray", alpha=1.0)
                    texto_color = color  # Mismo color que la gráfica
                else:  # Puntos máximos o finales
                    bbox_props_intermedio = bbox_props or dict(boxstyle="round,pad=0.3,rounding_size=0.99", fc=color, ec="none", alpha=1.0)
                    texto_color = "white"  # Texto blanco para máximos/finales

                # Añadir la etiqueta
                plt.text(
                    x_pos,          # Posición X (misma que la barra)
                    y_etiqueta,      # Posición Y (encima del punto más alto/apilada)
                    f"{int(row[col]):,}",
                    fontsize=fontsize,
                    color=texto_color,  # Color del texto
                    ha='center',
                    va='bottom',
                    bbox=bbox_props_intermedio
                )

def areaplot2(df, columna_fecha, tipografia=None):
    """
    Genera una gráfica de área (area plot) a partir de un DataFrame.

    Args:
        df (pd.DataFrame): DataFrame que contiene los datos a graficar.
        columna_fecha (str): Nombre de la columna que se usará como eje X.
        tipografia (FontProperties, optional): Fuente personalizada para los textos. Por defecto es None.
    """
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 24, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 24, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 9, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }
    
    # Verificar que la columna especificada exista en el DataFrame
    if columna_fecha not in df.columns:
        raise ValueError(f"La columna '{columna_fecha}' no existe en el DataFrame.")
    
    # Configurar el tamaño de la figura en píxeles
    ancho_px = 1480
    alto_px = 520
    dpi = 100  # Resolución en píxeles por pulgada
    ancho_in = ancho_px / dpi
    alto_in = alto_px / dpi
    
    # Crear la figura con el tamaño especificado
    fig, ax = plt.subplots(figsize=(ancho_in, alto_in), dpi=dpi)
    
    # Obtener los colores
    colores_asignados = ["#006157", "#767676", "#671435", "#9B2247", "#9D792A", "#D5B162"]
    
    # Obtener las columnas a graficar (excluyendo la columna de fecha)
    columnas_a_graficar = [col for col in df.columns if col != columna_fecha]
    
    # Graficar el área para cada columna
    ax.stackplot(
        df[columna_fecha],
        [df[col] for col in columnas_a_graficar],
        labels=columnas_a_graficar,
        colors=colores_asignados[:len(columnas_a_graficar)],
        alpha=0.8
    )

    # Configurar etiquetas y título
    ax.set_xlabel(columna_fecha, fontdict=font_config['etiquetas_eje_x'])
    ax.set_ylabel("Valores", fontdict=font_config['etiquetas_eje_y'])

    # Configurar las etiquetas del eje X
    ax.tick_params(axis='x', labelsize=font_config['etiquetas_eje_x']['size'], labelcolor=font_config['etiquetas_eje_x']['color'])
    ax.tick_params(axis='y', labelsize=font_config['etiquetas_eje_y']['size'], labelcolor=font_config['etiquetas_eje_y']['color'])

    # Posicionar el eje X en y=0
    ax.spines['bottom'].set_position(('data', 0))

    # Rotar las etiquetas del eje X
    plt.xticks(rotation=90)
    
    # Desactivar o activar bordes
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(True)
    
    # Asignar grosor a los ejes visibles
    ax.spines['bottom'].set_linewidth(2)  # Grosor del eje inferior
    
    # Mantener las líneas del grid
    ax.grid(axis='y', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
    ax.grid(axis='x', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)

    # Ajustar etiquetas en los puntos máximos de cada columna
    ajusta_etiquetas_apiladas_manual(
        dataframe=df,
        columnas=columnas_a_graficar,
        colores=colores_asignados,
        columna_x=columna_fecha,
        fontsize=font_config['capsula_valor']['size'],
        separacion=1100
    )

    # Calcular el valor máximo de las etiquetas y agregar un margen
    y_max = df[columnas_a_graficar].sum(axis=1).max()  # Suma acumulada máxima
    margen = y_max * 0.5  # Margen del 10%
    ax.set_ylim(0, y_max + margen)  # Ajustar el límite del eje Y

    # Guardar la gráfica como archivo SVG
    #plt.savefig("areaplot2.png", format="png", bbox_inches='tight', dpi=dpi)
    plt.savefig("areaplot2.svg", format="svg", bbox_inches='tight', dpi=dpi)

# Llamar a la función con el DataFrame df, la columna 'Fecha' y la fuente personalizada
areaplot2(dataframe_a_graficar, columna_fecha=columna_eje_x, tipografia=arial_font) 
Ver carpeta en GitHub
Barras apiladas horizontales
Ejemplo gráfico areaplot

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager

# VARIABLES A CAMBIAR:
dataframe_a_graficar = data

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def calcular_intervalo(max_valor):
    """Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
    potencia = 10 ** (int(np.log10(max_valor)) - 1)
    intervalo = round(max_valor / 5 / potencia) * potencia

    # Asegurar que el intervalo no sea muy pequeño o grande
    if max_valor / intervalo > 8:
        intervalo *= 2
    elif max_valor / intervalo < 4:
        intervalo /= 2

    return max(intervalo, 1)  # Mínimo intervalo de 1

def barras_apiladas_horizontales(data_vertical, bar_width=0.6):
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 32, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }

    categorias = ['Hombre', 'Mujer', 'No identificado']
    data['Total'] = data[categorias].sum(axis=1)
    sorted_data = data.sort_values(by='Total', ascending=False)
    entidades = sorted_data.index.tolist()

    max_valor = sorted_data['Total'].max()
    total = sorted_data['Total'].sum()
    x_max = max_valor * 1.15
    x_interval = max_valor // 5 or 1

    fig, ax = plt.subplots(figsize=(10, 14), dpi=300)
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    x_ticks = np.arange(0, x_max + x_interval, x_interval)
    for x in x_ticks:
        lw = 2 if x == max_valor else 0.75
        lc = 'black' if x == max_valor else '#B9B9B9'
        ax.axvline(x, color=lc, linewidth=lw)

    ax.axvline(0, color='black', linewidth=2, zorder=1)

    offset_capsula = x_max * 0.015
    offset_porcentaje = x_max * 0.09
    bar_positions = np.arange(len(entidades))

    colores = {
        'Hombre': '#4C6A67',
        'Mujer': '#627B78',
        'No identificado': '#6F8583'
    }
    categorias = list(colores.keys());

    for i, entidad in enumerate(entidades):
        row = sorted_data.loc[entidad]
        y_pos = bar_positions[i]
        x_left = 0

        for cat in categorias:
            valor = row[cat]
            ax.barh(y_pos, valor, height=bar_width, left=x_left,
                    color=colores[cat], edgecolor='none', zorder=2)
            x_left += valor

        total_valor = row['Total']
        porcentaje = round((total_valor / total) * 100, 1)

        texto_kwargs = dict(
            ha='left',
            va='center',
            fontsize=font_config['capsula_valor']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['capsula_valor']['weight'],
            color=font_config['capsula_valor']['color']
        )

        if total_valor <= 5:
            boxstyle = "round,pad=0.4,rounding_size=0.59"
        elif total_valor <= 50:
            boxstyle = "round,pad=0.3,rounding_size=0.7"
        elif total_valor <= 100:
            boxstyle = "round,pad=0.3,rounding_size=0.79"
        else:
            boxstyle = "round,pad=0.3,rounding_size=0.9"

        bbox_props = dict(
            boxstyle=boxstyle,
            facecolor='white',
            edgecolor='#002F2A',
            linewidth=1.5
        )

        ax.text(
            total_valor + offset_capsula, y_pos, f"{int(total_valor):,}",
            bbox=bbox_props,
            **texto_kwargs
        )

        ax.text(
            total_valor + offset_porcentaje, y_pos, f"{porcentaje}%",
            color=font_config['porcentaje']['color'],
            fontsize=font_config['porcentaje']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['porcentaje']['weight'],
            ha='left',
            va='center'
        )

    ax.set_yticks(bar_positions)
    ax.set_yticklabels(
        entidades,
        fontsize=font_config['eje_y']['size'],
        fontfamily=font_config['family'],
        fontweight=font_config['eje_y']['weight']
    )

    ax.set_xlim(0, x_max)
    ax.invert_yaxis()

    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.tick_params(
        axis='x',
        labelsize=font_config['eje_x']['size'],  # Tamaño de la fuente
        labelcolor=font_config['eje_x']['color']  # Color de la fuente
    )
    ax.grid(False)

    plt.tight_layout()

    # Guardar la gráfica como archivo SVG
    plt.savefig("barras_apiladas_horizontales.svg", format="svg", bbox_inches='tight')
    plt.show()

barras_apiladas_horizontales(dataframe_a_graficar)
Ver carpeta en GitHub
Barras apiladas verticales

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager

# VARIABLES A CAMBIAR:
dataframe_a_graficar = data

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def calcular_intervalo(max_valor):
    """Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
    potencia = 10 ** (int(np.log10(max_valor)) - 1)
    intervalo = round(max_valor / 5 / potencia) * potencia

    # Asegurar que el intervalo no sea muy pequeño o grande
    if max_valor / intervalo > 8:
        intervalo *= 2
    elif max_valor / intervalo < 4:
        intervalo /= 2

    return max(intervalo, 1)  # Mínimo intervalo de 1

def barras_apiladas_verticales(data, bar_width=0.6):
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 12, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 32, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }

    categorias = ['Hombre', 'Mujer', 'No identificado']
    data['Total'] = data[categorias].sum(axis=1)
    sorted_data = data.sort_values(by='Total', ascending=False)
    entidades = sorted_data.index.tolist()

    max_valor = sorted_data['Total'].max()
    total = sorted_data['Total'].sum()

    y_max = max_valor * 1.15
    y_interval = calcular_intervalo(max_valor)

    fig, ax = plt.subplots(figsize=(16, 6), dpi=300)
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    y_ticks = np.arange(0, y_max + y_interval, y_interval)
    for y in y_ticks:
        lw = 2 if y == y_max else 0.75
        lc = 'black' if y == y_max else '#B9B9B9'
        ax.axhline(y, color=lc, linewidth=lw)

    ax.axhline(0, color='black', linewidth=2, zorder=1)

    offset_capsula = y_max * 0.05  
    offset_porcentaje = y_max * 0.15 
    bar_positions = np.arange(len(entidades))

    colores = {
        'Hombre': '#4C6A67',
        'Mujer': '#627B78',
        'No identificado': '#6F8583'
    }
    categorias = list(colores.keys());

    for i, entidad in enumerate(entidades):
        row = sorted_data.loc[entidad]
        x_pos = bar_positions[i]
        y_bottom = 0

        for cat in categorias:
            valor = row[cat]
            ax.bar(x_pos, valor, width=bar_width, bottom=y_bottom,
                   color=colores[cat], edgecolor='none', zorder=2)
            y_bottom += valor

        total_valor = row['Total']
        porcentaje = round((total_valor / total) * 100, 1)

        # Estilo de texto
        texto_kwargs = dict(
            ha='center',
            va='bottom',
            fontsize=font_config['capsula_valor']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['capsula_valor']['weight'],
            color=font_config['capsula_valor']['color']
        )

        # Ajuste de estilo de cápsula según el valor
        if total_valor <= 5:
            boxstyle = "round,pad=0.4,rounding_size=0.59"
        elif total_valor <= 50:
            boxstyle = "round,pad=0.3,rounding_size=0.7"
        elif total_valor <= 100:
            boxstyle = "round,pad=0.3,rounding_size=0.79"
        else:
            boxstyle = "round,pad=0.3,rounding_size=0.9"

        bbox_props = dict(
            boxstyle=boxstyle,
            facecolor='white',
            edgecolor='#002F2A',
            linewidth=1.5
        )

        # Cápsula con total
        ax.text(
            x_pos, total_valor + offset_capsula, f"{int(total_valor):,}",
            bbox=bbox_props,
            **texto_kwargs
        )

        # Porcentaje encima
        ax.text(
            x_pos, total_valor + offset_porcentaje, f"{porcentaje}%",
            color=font_config['porcentaje']['color'],
            fontsize=font_config['porcentaje']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['porcentaje']['weight'],
            ha='center',
            va='bottom'
        )

    # Eje X
    ax.set_xticks(bar_positions)
    ax.set_xticklabels(
        entidades, rotation=90,
        fontsize=font_config['eje_x']['size'],
        fontfamily=font_config['family'],
        fontweight=font_config['eje_x']['weight']
    )

    ax.set_ylim(0, y_max)
    ax.set_xlim(bar_positions[0] - bar_width, bar_positions[-1] + bar_width)

    # Limpiar bordes y ticks
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.tick_params(axis='both', which='both', length=0)
    ax.grid(False)

    plt.tight_layout()

    # Guardar la gráfica como archivo SVG
    plt.savefig("barras_apiladas_verticales.svg", format="svg", bbox_inches='tight')
    plt.show()

barras_apiladas_verticales(dataframe_a_graficar)
Ver carpeta en GitHub
Barras horizontales

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path

# VARIABLES A CAMBIAR:
dataframe_a_graficar = victimas_por_entidad

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def calcular_intervalo(max_valor):
    """Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
    potencia = 10 ** (int(np.log10(max_valor)) - 1)
    intervalo = round(max_valor / 5 / potencia) * potencia

    # Asegurar que el intervalo no sea muy pequeño o grande
    if max_valor / intervalo > 8:
        intervalo *= 2
    elif max_valor / intervalo < 4:
        intervalo /= 2

    return max(intervalo, 1)  # Mínimo intervalo de 1

def barras_horizontales(victimas_por_entidad, bar_height=0.6):
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 18, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 32, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }

    # Ordenar de mayor a menor 
    sorted_items = sorted(victimas_por_entidad.items(), key=lambda x: x[1], reverse=True)
    entidades = [item[0] for item in sorted_items]
    valores = [item[1] for item in sorted_items]
    max_valor = max(valores)
    total = sum(valores)

   
    y_positions = np.arange(len(entidades)) 

    
    fig, ax = plt.subplots(figsize=(10, 14), dpi=300)
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    x_max = max_valor * 1.5
    x_interval = max_valor // 5 or 1

    # Líneas verticales
    x_ticks = np.arange(0, x_max + x_interval, x_interval)
    for x in x_ticks:
        lw = 2 if x == max_valor else 0.75
        lc = 'black' if x == max_valor else '#B9B9B9'
        ax.axvline(x, color=lc, linewidth=lw)

    # Dibujar barras
    for y_pos, entidad, valor in zip(y_positions, entidades, valores):
        porcentaje = round((valor / total) * 100, 1)

        if valor == max_valor:
            for alpha in np.linspace(0.3, 1, 10):
                ax.barh(y_pos, valor, height=bar_height,
                        color='#8B0000', alpha=alpha, edgecolor='none', zorder=2)
            bbox_color = '#8B0000'
            text_color = font_config['capsula_max']['color']
            edge_col = 'none'
            lw = 0
        else:
            for alpha in np.linspace(0.1, 0.8, 10):
                ax.barh(y_pos, valor, height=bar_height,
                        color='#10302C', alpha=alpha, edgecolor='none', zorder=2)
            bbox_color = 'white'
            text_color = font_config['capsula_valor']['color']
            edge_col = '#002F2A'
            lw = 1.5

        # Cápsula con valor
        ax.text(
            total_valor + offset_capsula, y_pos, f"{valor:,}",
            bbox=dict(boxstyle="round,pad=0.3,rounding_size=0.7",
                          facecolor=bbox_color,
                          edgecolor=edge_col,
                          linewidth=lw),
            ha='left',
            va='center',
            fontsize=font_config['capsula_valor']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['capsula_valor']['weight'],
            color=text_color
        )

        # Porcentaje
        ax.text(
            total_valor + offset_porcentaje, y_pos, f"{porcentaje}%",
            color=font_config['porcentaje']['color'],
            fontsize=font_config['porcentaje']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['porcentaje']['weight'],
            ha='left',
            va='center'
        )

    # Eje Y (etiquetas)
    ax.set_yticks(y_positions)
    ax.set_yticklabels(
        entidades,
        fontsize=font_config['eje_y']['size'],
        fontfamily=font_config['family'],
        fontweight=font_config['eje_y']['weight']
    )

    ax.set_xlim(0, x_max)
    ax.invert_yaxis()

    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.tick_params(
        axis='x',
        labelsize=font_config['eje_x']['size'],  # Tamaño de la fuente
        labelcolor=font_config['eje_x']['color']  # Color de la fuente
    )
    ax.grid(False)

    plt.tight_layout()

    # Guardar la gráfica como archivo SVG
    plt.savefig("barras_horizontales.svg", format="svg", bbox_inches='tight')
    plt.show()

barras_horizontales(dataframe_a_graficar)
Ver carpeta en GitHub
Barras verticales

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path

# VARIABLES A CAMBIAR:
dataframe_a_graficar = victimas_por_entidad

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def calcular_intervalo(max_valor):
    """Calcula un intervalo adecuado para el eje Y basado en el valor máximo"""
    potencia = 10 ** (int(np.log10(max_valor)) - 1)
    intervalo = round(max_valor / 5 / potencia) * potencia

    # Asegurar que el intervalo no sea muy pequeño o grande
    if max_valor / intervalo > 8:
        intervalo *= 2
    elif max_valor / intervalo < 4:
        intervalo /= 2

    return max(intervalo, 1)  # Mínimo intervalo de 1

def barras_verticales(victimas_por_entidad, bar_width=0.6):
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 12, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 12, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 12, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 12, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 14, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }

    categorias = ['Hombre', 'Mujer', 'No identificado']
    data['Total'] = data[categorias].sum(axis=1)
    sorted_data = data.sort_values(by='Total', ascending=False)
    entidades = sorted_data.index.tolist()

    max_valor = sorted_data['Total'].max()
    total = sorted_data['Total'].sum()

    y_max = max_valor * 1.15
    y_interval = calcular_intervalo(max_valor)

    fig, ax = plt.subplots(figsize=(16, 6), dpi=300)
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    y_ticks = np.arange(0, y_max + y_interval, y_interval)
    for y in y_ticks:
        lw = 2 if y == y_max else 0.75
        lc = 'black' if y == y_max else '#B9B9B9'
        ax.axhline(y, color=lc, linewidth=lw)

    ax.axhline(0, color='black', linewidth=2, zorder=1)

    offset_capsula = y_max * 0.05  
    offset_porcentaje = y_max * 0.15 
    bar_positions = np.arange(len(entidades))

    colores = {
        'Hombre': '#4C6A67',
        'Mujer': '#627B78',
        'No identificado': '#6F8583'
    }
    categorias = list(colores.keys());

    for i, entidad in enumerate(entidades):
        row = sorted_data.loc[entidad]
        x_pos = bar_positions[i]
        y_bottom = 0

        for cat in categorias:
            valor = row[cat]
            ax.bar(x_pos, valor, width=bar_width, bottom=y_bottom,
                   color=colores[cat], edgecolor='none', zorder=2)
            y_bottom += valor

        total_valor = row['Total']
        porcentaje = round((total_valor / total) * 100, 1)

        # Estilo de texto
        texto_kwargs = dict(
            ha='center',
            va='bottom',
            fontsize=font_config['capsula_valor']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['capsula_valor']['weight'],
            color=font_config['capsula_valor']['color']
        )

        # Ajuste de estilo de cápsula según el valor
        if total_valor <= 5:
            boxstyle = "round,pad=0.4,rounding_size=0.59"
        elif total_valor <= 50:
            boxstyle = "round,pad=0.3,rounding_size=0.7"
        elif total_valor <= 100:
            boxstyle = "round,pad=0.3,rounding_size=0.79"
        else:
            boxstyle = "round,pad=0.3,rounding_size=0.9"

        bbox_props = dict(
            boxstyle=boxstyle,
            facecolor='white',
            edgecolor='#002F2A',
            linewidth=1.5
        )

        # Cápsula con total
        ax.text(
            x_pos, total_valor + offset_capsula, f"{int(total_valor):,}",
            bbox=bbox_props,
            **texto_kwargs
        )

        # Porcentaje encima
        ax.text(
            x_pos, total_valor + offset_porcentaje, f"{porcentaje}%",
            color=font_config['porcentaje']['color'],
            fontsize=font_config['porcentaje']['size'],
            fontfamily=font_config['family'],
            fontweight=font_config['porcentaje']['weight'],
            ha='center',
            va='bottom'
        )

    # Eje X
    ax.set_xticks(bar_positions)
    ax.set_xticklabels(
        entidades, rotation=90,
        fontsize=font_config['eje_x']['size'],
        fontfamily=font_config['family'],
        fontweight=font_config['eje_x']['weight']
    )

    ax.set_ylim(0, y_max)
    ax.set_xlim(bar_positions[0] - bar_width, bar_positions[-1] + bar_width)

    # Limpiar bordes y ticks
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.tick_params(axis='both', which='both', length=0)
    ax.grid(False)

    plt.tight_layout()

    # Guardar la gráfica como archivo SVG
    plt.savefig("barras_verticales.svg", format="svg", bbox_inches='tight')
    plt.show()

barras_verticales(dataframe_a_graficar)
Ver carpeta en GitHub
Gráfica de línea

from pathlib import Path
import matplotlib.transforms as mtrans
from matplotlib.text import TextPath
from matplotlib.patches import PathPatch
import matplotlib.pyplot as plt
from matplotlib.dates import date2num
import matplotlib.font_manager as font_manager

# VARIABLES A CAMBIAR
dataframe_a_graficar = df

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def curly_at_fechas(x, y, width, height, ax=None, color="k"):
    """
    Dibuja una llave '{' o '}' en cualquier lugar del gráfico.

    Parámetros:
    - x: Coordenada X (puede ser una fecha o un valor numérico).
    - y: Coordenada Y.
    - width: Ancho de la llave.
    - height: Altura de la llave.
    - ax: Eje de Matplotlib donde se dibujará la llave (opcional).
    - color: Color del símbolo de la llave (por defecto es negro).
    """
    if not ax:
        ax = plt.gca()
    
    # Si x es una fecha, convertirla a un valor numérico
    if isinstance(x, pd.Timestamp):
        x = date2num(x)
    
    # Crear el símbolo de la llave con una fuente explícita
    tp = TextPath((0, 0), "}", size=1, prop=dict(family="DejaVu Sans"))
    
    # Escalar y trasladar la llave
    trans = (
        mtrans.Affine2D().scale(width, height) +
        mtrans.Affine2D().translate(x, y) +
        ax.transData
    )
    
    # Crear y añadir el PathPatch al eje con el color especificado
    pp = PathPatch(tp, lw=0, fc=color, transform=trans)
    ax.add_artist(pp)


def lineal(df, columna_fecha, columna_grafica, columna_linea, tipografia=None):
    """Genera una gráfica de área (area plot) para una columna específica de un DataFrame."""
    # Verificar que las columnas especificadas existan en el DataFrame
    if columna_fecha not in df.columns:
        raise ValueError(f"La columna '{columna_fecha}' no existe en el DataFrame.")
    if columna_grafica not in df.columns:
        raise ValueError(f"La columna '{columna_grafica}' no existe en el DataFrame.")
    if columna_linea not in df.columns:
        raise ValueError(f"La columna '{columna_linea}' no existe en el DataFrame.")
    
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 24, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 24, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 9, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size': 18, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 10, 'weight': 'medium', 'color': '#4C6A67'},
        'leyenda': {'size': 20, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }
    
    # Configurar el tamaño de la figura en píxeles
    ancho_px = 1480
    alto_px = 520
    dpi = 100  # Resolución en píxeles por pulgada
    ancho_in = ancho_px / dpi
    alto_in = alto_px / dpi
    
    # Crear la figura con el tamaño especificado
    fig, ax = plt.subplots(figsize=(ancho_in, alto_in), dpi=dpi)
    
    # Seleccionar colores específicos
    color_area = '#d4dce9'  # Primer color de la lista
    color_linea = '#2f5597'  # Segundo color de la lista
    color_linea_punteada = '#c00000'  # Color para la línea punteada
    
    # Graficar el área
    ax.fill_between(
        df[columna_fecha],
        df[columna_grafica],
        color=color_area,  # Color del área
        alpha=1.,
    )
    
    # Graficar la línea sobre el área
    ax.plot(
        df[columna_fecha],
        df[columna_grafica],
        color=color_linea,  # Color de la línea
        linewidth=2
    )
    
    # Graficar la línea punteada
    ax.plot(
        df[columna_fecha],
        df[columna_linea],
        color=color_linea_punteada,  # Color de la línea punteada
        linestyle='--',  # Estilo de línea punteada
        linewidth=2
    )
    
    # Configurar etiquetas y título
    ax.set_xlabel(columna_fecha, fontdict=font_config['eje_x'])
    ax.set_ylabel("Valores", fontdict=font_config['eje_y'])

    # Configurar las etiquetas del eje X
    ax.tick_params(axis='x', labelsize=font_config['etiquetas_eje_x']['size'], labelcolor=font_config['etiquetas_eje_x']['color'])
    ax.tick_params(axis='y', labelsize=font_config['etiquetas_eje_y']['size'], labelcolor=font_config['etiquetas_eje_y']['color'])

    # Posicionar el eje X en y=0
    ax.spines['bottom'].set_position(('data', 0))

    # Rotar las etiquetas del eje X
    plt.xticks(rotation=90)
    
    # Desactivar o activar bordes
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(True)
    
    # Asignar grosor a los ejes visibles
    ax.spines['bottom'].set_linewidth(2)  # Grosor del eje inferior
    
    # Mantener las líneas del grid
    ax.grid(axis='y', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
    ax.grid(axis='x', linestyle='-', color='#000000', alpha=0.2, linewidth=0.75)
    
    # Convertir la fecha 2024 a un índice o posición en el eje X
    x_pos = df["Fecha"].iloc[-1]
    y_pos_gra = df[columna_grafica].iloc[-1]
    y_pos_lin = df[columna_linea].iloc[-1]
    if y_pos_gra > y_pos_lin:
        y_pos_min = y_pos_lin
        y_pos_max = y_pos_gra
    else:   
        y_pos_min = y_pos_gra
        y_pos_max = y_pos_lin
    altura = y_pos_max - y_pos_min

    # Calcular los factores proporcionales
    x_min, x_max = ax.get_xlim()  # Obtener los límites del eje X
    y_min, y_max = ax.get_ylim()  # Obtener los límites del eje Y

    factor_ancho = 0.04 * (x_max - x_min)  # Proporcional al rango del eje X
    factor_alto = 0.05 * (y_max - y_min)   # Proporcional al rango del eje Y

    # Añadir el símbolo de la llave
    curly_at_fechas(x_pos, y_pos_min+0.2*factor_alto, width=factor_ancho, height=altura+factor_alto, ax=ax, color="#af0b19")
    
    # Calcular la diferencia entre y_pos_gra y y_pos_lin
    diferencia = round(y_pos_gra - y_pos_lin)

    # Añadir una cápsula con la diferencia en la parte derecha de la gráfica
    color_capsula = "#af0b19"  # Color de la cápsula
    bbox_props = dict(boxstyle="round,pad=0.25,rounding_size=0.99", fc=color_capsula, ec="none", alpha=1.0)

    x_pos_numeric = date2num(x_pos)  # Convertir la fecha a un número

    # Posicionar la cápsula en la parte derecha de la gráfica
    ax.text(
        x_pos_numeric + 1.8 * factor_ancho,  # Usar el valor numérico de x_pos
        y_pos_min + altura / 2.5,  # Centrar verticalmente entre y_pos_gra y y_pos_lin
        diferencia,  # Texto de la cápsula
        color="white",  # Color del texto
        fontsize=font_config['capsula_max']['size'],  # Tamaño de la fuente
        bbox=bbox_props,  # Estilo de la cápsula
        ha="center",  # Alinear horizontalmente
        va="center"   # Alinear verticalmente
    )
    
    # Guardar la gráfica como archivo SVG
    plt.savefig("linea.svg", format="svg", bbox_inches='tight', dpi=dpi)
    plt.show()

# Ejemplo de uso
lineal(dataframe_a_graficar, columna_fecha="Fecha", columna_grafica="Columna1", columna_linea="Columna3")
Ver carpeta en GitHub
Areaplot con snapshot

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from scipy.interpolate import make_interp_spline

# VARIABLES A CAMBIAR:
dataframe_a_graficar = data

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.fontManager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))

def snapshot(data):
    """
    Genera un gráfico de área apilada con los datos proporcionados en un DataFrame.

    Parámetros:
    - data: DataFrame que contiene las columnas 'fechas', 'comisiones', 'fiscalias' y 'portal'.
    """
    font_config = {
        'family': 'Arial',  # Cambiar a Arial
        'titulo': {'size': 36, 'weight': 'medium', 'color': '#000000'},
        'eje_y': {'size': 24, 'weight': 'medium', 'color': '#000000'},
        'eje_x': {'size': 24, 'weight': 'medium', 'color': '#000000'},
        'etiquetas_eje_y': {'size': 24, 'weight': 'medium', 'color': '#767676'},
        'etiquetas_eje_x': {'size': 24, 'weight': 'semibold', 'color': '#767676'},
        'capsula_valor': {'size': 20, 'weight': 'medium', 'color': '#10302C'},
        'capsula_max': {'size':  12, 'weight': 'medium', 'color': 'white'},
        'porcentaje': {'size': 20, 'weight': 'medium', 'color': 'white'},
        'leyenda': {'size': 30, 'weight': 'medium', 'color': '#767676'}  # Nueva categoría para la leyenda
    }

    # Extraer columnas del DataFrame
    fechas = data['fechas']
    comisiones = data['comisiones']
    fiscalias = data['fiscalias']
    portal = data['portal']
    
    # Crear figura
    fig, ax = plt.subplots(figsize=(16, 8))
    n = len(fechas)
    x = np.arange(n) + 0.5  # Desfase visual

    fig_sub = plt.figure(figsize=(16, 8))
    gs = fig.add_gridspec(1, 2, width_ratios=[3, 1], height_ratios=[1])

    # ==============================================
    # 2.1. PRIMER SUBPLOT - GRÁFICO DE ÁREAS
    # ==============================================
    ax1 = fig_sub.add_subplot(gs[0])  # Usa este, no crees otra figura

    # Suavizar las curvas visualmente
    x_suave = np.linspace(x.min(), x.max(), 300)  # Más puntos para suavizar
    comisiones_suave = make_interp_spline(x, comisiones)(x_suave)
    fiscalias_suave = make_interp_spline(x, fiscalias)(x_suave)
    portal_suave = make_interp_spline(x, portal)(x_suave)


    x = np.arange(n) + 0.5
    ax1.stackplot(x_suave, comisiones_suave, fiscalias_suave, portal_suave, labels=[
        'Comisiones', 'Fiscalías y Procuradurías', 'Portal'],
        colors=['#215F53', '#7570B3', '#C7EAE5'])

    # Eje X
    ax1.set_xticks(x)
    ax1.set_xticklabels(fechas, rotation=90, fontsize=font_config['etiquetas_eje_x']['size'], 
                         fontweight=font_config['etiquetas_eje_x']['weight'], 
                         color=font_config['etiquetas_eje_x']['color'])
    ax1.set_xlim(x[0] - 0.5, x[-1] + 0.5)

    # Eje Y
    ax1.set_yticks(np.linspace(0, 1, 5))
    ax1.set_yticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=font_config['etiquetas_eje_y']['size'], 
                         fontweight=font_config['etiquetas_eje_y']['weight'], 
                         color=font_config['etiquetas_eje_y']['color'])

    # Estética
    ax1.spines['top'].set_visible(False)
    ax1.spines['right'].set_visible(False)
    ax1.spines['left'].set_visible(False)
    ax1.spines['bottom'].set_linewidth(2)
    ax1.grid(axis='y', linestyle='-', color='white', alpha=0.8, linewidth=1.25)
    ax1.grid(axis='x', linestyle='-', color='white', alpha=0.3, linewidth=0.75)

    # Etiquetas de totales
    x_coords = [1.5, 6, 10.5]
    y_coords = [0.15, 0.45, 0.85]
    totales = [sum(comisiones), sum(fiscalias), sum(portal)]
    colores_borde = ['#D9D9D9'] * 3

    for x_c, y_c, total, color_borde in zip(x_coords, y_coords, totales, colores_borde):
        ax1.text(x_c, y_c, f"{total:.2f}",
                 ha='center', va='center',
                 fontsize=font_config['capsula_valor']['size'], 
                 color=font_config['capsula_valor']['color'],
                 bbox=dict(boxstyle="round,pad=0.6,rounding_size=1",
                           facecolor='white', edgecolor=color_borde, linewidth=1.8))

    # ==============================================
    # 2.2. SEGUNDO SUBPLOT - Snapshot
    # ==============================================
    ax2 = fig_sub.add_subplot(gs[1])

    # Datos simulados (sept. 2006)
    totales = np.array([sum(comisiones[:1]), sum(fiscalias[:1]), sum(portal[:1])])
    totales_norm = totales / totales.sum()

    colores = ['#215F53', '#7570B3', '#C7EAE5']
    bar_width = 0.5

    # Dibujar barra apilada
    bottom = 0
    for i, (valor, color) in enumerate(zip(totales_norm, colores)):
        ax2.bar(0, valor, width=bar_width, bottom=bottom,
                color=color, edgecolor='white', linewidth=0.5)
    
        # Etiqueta de porcentaje en el centro
        ax2.text(0, bottom + valor/2, f"{valor*100:.1f}%",
                 ha='center', va='center', color=font_config['porcentaje']['color'], 
                 fontweight=font_config['porcentaje']['weight'], 
                 fontsize=font_config['porcentaje']['size'])
        bottom += valor

    # Estética limpia
    ax2.set_xlim(-0.5, 0.5)
    ax2.set_ylim(0, 1)
    ax2.set_xticks([])
    ax2.set_yticks([])

    for spine in ax2.spines.values():
        spine.set_visible(False)
    
    # Eliminar etiquetas de fecha del segundo subplot
    ax2.tick_params(bottom=False, labelbottom=False)

    # Evitar espacio excesivo
    plt.subplots_adjust(wspace=0.05, hspace=0.05)

    # Crear una instancia de FontProperties para la leyenda
    leyenda_font = font_manager.FontProperties(
        family=font_config['family'],
        size=font_config['leyenda']['size'],
        weight=font_config['leyenda']['weight']
    )

    # Leyenda común
    fig_sub.legend(
        ['Comisiones', 'Fiscalías', 'Portal'],
        loc='upper center',
        ncol=3,
        frameon=False,
        fontsize=font_config['leyenda']['size'],
        prop=leyenda_font,  # Usar la instancia de FontProperties
        bbox_to_anchor=(0.5, 1.05)  # Ajustar la posición vertical (1.05 es un poco más arriba)
    )    

    plt.tight_layout()

    # Guardar la gráfica como archivo SVG
    plt.savefig("snapshot.svg", format="svg", bbox_inches='tight')
    plt.show()

snapshot(dataframe_a_graficar)
Ver carpeta en GitHub
Treemap

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
import squarify 

# VARIABLES A CAMBIAR:
dataframe_a_graficar = victimas_por_entidad

# Configurar las fuentes SVG como texto editable
plt.rcParams['svg.fonttype'] = 'none'

# Ruta relativa para las fuentes
font_dirs = [Path("fonts/arial")]
font_files = font_manager.findSystemFonts(fontpaths=font_dirs)

for font_file in font_files:
    font_manager.addfont(font_file)

# Crear una instancia de FontProperties para Arial
arial_font = font_manager.FontProperties(fname=str(font_files[0]))


def treemap(victimas_por_entidad):
    if not isinstance(victimas_por_entidad, pd.Series):
        raise ValueError("Los datos deben ser una Serie de pandas")

    # Asegurar que todos los valores sean numéricos tipo float
    victimas_por_entidad = pd.to_numeric(victimas_por_entidad, errors='coerce').fillna(0).astype(float)

    # Configuración de fuentes y colores
    font_config = {
        'family': 'Arial',
        'titulo': {'size': 18, 'weight': 'bold', 'color': '#10302C'},
        'etiquetas': {'size': 10, 'weight': 'bold', 'color': 'white'},
        'porcentaje': {'size': 9, 'weight': 'medium', 'color': '#A57F2C'}
    }

    try:
        # Crear dataframe desde la serie
        df = pd.DataFrame({
            'ENTIDAD': victimas_por_entidad.index,
            'Total': victimas_por_entidad.values
        })
        df.set_index('ENTIDAD', inplace=True)

        # Ordenar y calcular porcentaje
        df = df.sort_values(by='Total', ascending=False)
        total_nacional = df['Total'].sum()
        df['Porcentaje'] = (df['Total'] / total_nacional * 100).round(1)
    except Exception as e:
        print(f"Error: {e}")
        return None

    # Colores
    max_valor = df['Total'].max()
    colores = ['#10302C' if val == max_valor else '#4C6A67' for val in df['Total']]

    # Configurar la figura
    plt.rc('font', family=font_config['family'])
    fig, ax = plt.subplots(figsize=(16, 10), dpi=300)
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    # rectangulos
    sizes = df['Total'].tolist()
    rectangles = squarify.normalize_sizes(sizes, 1, 1)
    rectangles = squarify.squarify(rectangles, 0, 0, 1, 1)

    for rect, (entidad, row), color in zip(rectangles, df.iterrows(), colores):
        x, y, dx, dy = rect['x'], rect['y'], rect['dx'], rect['dy']

        ax.add_patch(plt.Rectangle(
            (x, y), dx, dy,
            facecolor=color,
            edgecolor='white',
            linewidth=5
        ))

        area = dx * dy

        if area > 0.001:
            palabras = entidad.split()
            if len(palabras) > 2:
                entidad_mod = ' '.join(palabras[:2]) + '\n' + ' '.join(palabras[2:])
            else:
                entidad_mod = entidad

            etiqueta = f"{entidad_mod}\n{int(row['Total']):,}\n{row['Porcentaje']}%"

            # Ajuste automático de tamaño de fuente según área
            if area > 0.02:
                fontsize = 10
            elif area > 0.01:
                fontsize = 8
            else:
                fontsize = 6

            x_text = x + dx * 0.04
            y_text = y + dy * 0.88

            ax.text(
                x_text,
                y_text,
                etiqueta,
                ha='left',
                va='top',
                fontsize=fontsize,
                fontweight=font_config['etiquetas']['weight'],
                color=font_config['etiquetas']['color'],
                zorder=10
            )

    ax.axis('off')

    plt.tight_layout()

    # Guardar la gráfica como archivo SVG
    plt.savefig("treemap.svg", format="svg", bbox_inches='tight')
    plt.show()

treemap(dataframe_a_graficar)
Ver carpeta en GitHub
Barras de tendencia

# Crear una gráfica por estado
for estado in estados:
    fig, ax = plt.subplots(figsize=(12, 6))
    df_estado = df[df['estado'] == estado]
    pivot_df = df_estado.pivot(index='fecha', columns='indicador', values='valor').fillna(0)
    pivot_df[['Con datos', 'Sin datos']].plot(kind='bar', stacked=True, color=["#584290", "#b1adcf"], ax=ax, width=1)

    ax.set_title(f'{estado} - Casos con y sin datos', fontsize=14, fontweight='bold')
    ax.set_xlabel('Fecha')
    ax.set_ylabel('Número de casos')
    ax.set_xticks(range(0, len(pivot_df), 12))
    ax.set_xticklabels([date.strftime('%Y') for date in pivot_df.index[::12]], rotation=45, fontsize=8)
    ax.legend(title='')
    ax.grid(False)
    plt.tight_layout()
    plt.show()

    file_base = os.path.join(output_dir, f"{estado.replace(' ', '_')}_grafica_barras_tendencias")
    fig.savefig(f"{file_base}.png", format="png", bbox_inches='tight', dpi=300)
    fig.savefig(f"{file_base}.svg", format="svg", bbox_inches='tight', dpi=300)
    plt.close(fig)
Ver carpeta en GitHub
Barras verticales simples

# EXPORTAR: barras_apiladas

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess

def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
    subprocess.run([
        'scour', '-i', archivo_entrada, '-o', archivo_salida,
        '--enable-viewboxing', '--enable-id-stripping',
        '--shorten-ids', '--remove-descriptive-elements'
    ], check=True)

def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
    subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)

def formato_fechas(fechas):
    fechas = pd.to_datetime(fechas)
    # Si todos los años son diferentes, mostrar solo el año
    if len(set(f.year for f in fechas)) == len(fechas):
        return [str(f.year) for f in fechas]
    else:
        return [f.strftime("%d-%m-%Y") for f in fechas]

def barras_apiladas(
    df_wide, 
    nombre=None,
    font='Montserrat',
    fontsize_barra=15, 
    fontsize_valor_total=20,
    bar_height=0.5, 
    aumenta_ancho_fig=0,
    aumenta_alto_fig=0,
    aumenta_sep_leyenda=0.0,
    valor_barra=False, 
    valor_total=True,
    porcentaje_barra=False, 
    porcentaje_total=False, 
    porcentaje_total_inicio=False,
    orientacion='horizontal',
    ordenar_por='valor',      # 'valor' o 'etiqueta'
    orden='descendente',       # 'ascendente' o 'descendente'
    quitar_capsula=False,
    area_min=0,
    espacio_inicio=0,  # Espacio para el porcentaje total al inicio de la barra 
    paleta_colores=None,
    agregar_datos=None,
    asignar_etiquetas=None,
    grillas=True,
    leyenda=None,
    union_izquierda=0,
    union_derecha=0,       
    sustituir_etiquetas=None,
    separar_por_total=0.0,
    y_limits=None,
    nombre_eje_x=None,
    nombre_eje_y=None,
    resaltar_etiquetas=None
):
    # Configuración de la fuente
    font_config = {
        'family': font,
        'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
        'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
        'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
        'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
    }

    plt.rcParams['svg.fonttype'] = 'none'
    font_dirs = [Path("../0_fonts")]
    font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
    for font_file in font_files:
        font_manager.fontManager.addfont(font_file)

    nombre_df = nombre or "barras_apiladas"
    if paleta_colores:
        colores_asignados = paleta_colores
    else:
        colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]

    if isinstance(df_wide, pd.DataFrame):
        df_wide = df_wide.copy()
        categorias_x = df_wide.columns[0]
        # Conversión automática de años enteros a fechas
        if (
            pd.api.types.is_integer_dtype(df_wide[categorias_x])
            and df_wide[categorias_x].between(1900, 2100).all()
        ):
            df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
        df_wide = df_wide.set_index(categorias_x)
    else:
        raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")

    # --- UNIÓN DE LAS PRIMERAS N BARRAS ---
    etiqueta_union_izq = "Unión"
    if union_izquierda > 1 and len(df_wide) >= union_izquierda:
        union = df_wide.iloc[:union_izquierda].sum()
        idxs = df_wide.index[:union_izquierda]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            pd.DataFrame([union], index=[etiqueta_union_izq]),
            df_wide.iloc[union_izquierda:]
        ])

    # --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
    etiqueta_union_der = "Unión"
    if union_derecha > 1 and len(df_wide) >= union_derecha:
        union = df_wide.iloc[-union_derecha:].sum()
        idxs = df_wide.index[-union_derecha:]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            df_wide.iloc[:-union_derecha],
            pd.DataFrame([union], index=[etiqueta_union_der])
        ])

    # --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
    df_wide = df_wide.copy()
    etiquetas_personalizadas = {}
    if agregar_datos is not None:
        filas_extra = []
        for item in agregar_datos:
            # Permite tuplas de 2 o 3 elementos
            if len(item) == 3:
                categoria, etiqueta, valor = item
                etiquetas_personalizadas[categoria] = etiqueta
            else:
                categoria, valor = item
                etiqueta = None
            if isinstance(valor, (list, tuple, np.ndarray)):
                fila = dict(zip(df_wide.columns, valor))
            else:
                fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
            filas_extra.append(pd.Series(fila, name=categoria))
        df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])

    # --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
    if agregar_datos is not None:
        # Determina el tipo de índice para la barra espaciadora
        if isinstance(df_wide.index[0], (int, np.integer, str)):
            categoria_espaciadora = "__espaciador__"
        else:
            # Obtener la última fecha de agregar_datos y la primera fecha del resto
            fechas_agregar = [d[0] for d in agregar_datos]
            fecha_max_agregar = max(fechas_agregar)
            fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
            if fechas_resto:
                fecha_min_resto = min(fechas_resto)
                # Poner la fecha espaciadora justo entre ambas
                categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
            else:
                # Si no hay resto, solo suma un día a la última de agregar_datos
                categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
        fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
        n_agregar = len(agregar_datos)
        df_wide = pd.concat([
            df_wide.iloc[:n_agregar],                  # barras de agregar_datos
            pd.DataFrame([fila_espaciadora]),          # espaciador
            df_wide.iloc[n_agregar:]                   # el resto
        ])

    etiquetas_finales = None
    if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
        etiquetas_finales = df_wide[asignar_etiquetas].copy()
        df_wide = df_wide.drop(columns=[asignar_etiquetas])        

    # Calcular totales
    suma_total = df_wide.sum(axis=1)
    total_general = suma_total.sum()

    # Definir x_max antes de dibujar las barras para evitar UnboundLocalError
    x_max = suma_total.max() * 1.15

    # ORDENAMIENTO
    if ordenar_por == 'valor':
        sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
    elif ordenar_por == 'etiqueta':
        # Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
        indices = list(df_wide.index)
        izq = []
        der = []
        centro = indices.copy()
        if union_izquierda > 1:
            izq = [etiqueta_union_izq]
            centro = [i for i in centro if i != etiqueta_union_izq]
        if union_derecha > 1:
            der = [etiqueta_union_der]
            centro = [i for i in centro if i != etiqueta_union_der]
        tipos = set(type(idx) for idx in centro)
        if len(tipos) > 1:
            centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
        else:
            centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
        sort_index = izq + centro_ordenado + der
    else:
        sort_index = df_wide.index  # sin ordenar si valor inválido

    df_wide = df_wide.loc[sort_index]
    suma_total = suma_total.loc[sort_index]


    entidades = df_wide.index.values

    # --- FORMATEO DE ETIQUETAS ---
    # Detectar índices de tipo fecha
    indices_fechas = [
        i for i, idx in enumerate(df_wide.index)
        if isinstance(idx, (pd.Timestamp, np.datetime64))
    ]
    # Formatear todas las fechas juntas si hay al menos una
    if indices_fechas:
        fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
    else:
        fechas_formateadas = []

    entidades_formateadas = []
    fecha_idx = 0
    for i, idx in enumerate(df_wide.index):
        if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
            entidades_formateadas.append(idx)
        elif isinstance(idx, (pd.Timestamp, np.datetime64)):
            entidades_formateadas.append(fechas_formateadas[fecha_idx])
            fecha_idx += 1
        else:
            entidades_formateadas.append(str(idx))

    es_fecha = any(
        isinstance(idx, (pd.Timestamp, np.datetime64))
        for idx in df_wide.index
        if not (
            (union_izquierda > 1 and idx == etiqueta_union_izq) or
            (union_derecha > 1 and idx == etiqueta_union_der)
        )
    )

    # --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
    if sustituir_etiquetas is not None:
        if len(sustituir_etiquetas) != len(entidades_formateadas):
            raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
        entidades_formateadas = list(sustituir_etiquetas)

    # Reemplaza etiquetas personalizadas si existen
    if etiquetas_personalizadas:
        for i, entidad in enumerate(df_wide.index):
            if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
                entidades_formateadas[i] = etiquetas_personalizadas[entidad]

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        # Detecta el valor exacto del espaciador
        if es_fecha:
            # El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
            fechas_agregar = [d[0] for d in agregar_datos]
            fechas_todas = list(df_wide.index)
            fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
            anio_espaciador = None
            for idx in fechas_resto:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[idx] == 0).all():
                    anio_espaciador = idx
                    break
            for i, entidad in enumerate(df_wide.index):
                if entidad == anio_espaciador:
                    entidades_formateadas[i] = "..."
        else:
            for i, entidad in enumerate(entidades_formateadas):
                if entidad == "__espaciador__":
                    entidades_formateadas[i] = "..."


    valores = [df_wide[col].values for col in df_wide.columns]
    categorias = df_wide.columns
    posiciones = np.arange(len(entidades))

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        for i, entidad in enumerate(entidades_formateadas):
            if entidad == "__espaciador__":
                entidades_formateadas[i] = "..."

    # --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
    if orientacion == 'vertical':
        longitudes = []
        for total_valor in suma_total:
            espacio = "\u00A0"
            texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
            longitudes.append(len(texto_capsula))
        if longitudes:
            moda_capsula_len = mode(longitudes)
        else:
            moda_capsula_len = 10
        extra_width = moda_capsula_len * 0.1
        base_width = max(12, len(entidades)*extra_width)
        fig_width = base_width + aumenta_ancho_fig
        fig_height = 8 + aumenta_alto_fig
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    else:
        fig_width = 12 + aumenta_ancho_fig          
        fig_height = 15 + aumenta_alto_fig                 
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    # ---------------------------------------------------------------

    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    # Dibujar barras apiladas
    for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
        left = 0
        for i, valor in enumerate(valores):
            color = colores_asignados[i % len(colores_asignados)]
            label = categorias[i] if pos == 0 else None
            if orientacion == 'horizontal':
                if valor[pos] > 0:
                    ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                        if porcentaje_barra:
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                        ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
                                fontsize=font_config['valor_porcentaje_barra']['size'],
                                fontfamily=font_config['family'],
                                fontweight=font_config['valor_porcentaje_barra']['weight'],
                                color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]
            else:
                if valor[pos] > 0:
                    ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        if orientacion == 'vertical' and valor_barra and porcentaje_barra:
                            # Valor arriba, porcentaje abajo
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
                                    va='bottom', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                            ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
                                    va='top', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size']-2,
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                        else:
                            texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                            if porcentaje_barra:
                                porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                                texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                            ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            es_espaciador = entidad == "__espaciador__"

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            # Detecta si el índice es el espaciador
            if es_fecha:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[entidad] == 0).all():
                    es_espaciador = True
            else:
                es_espaciador = entidad == "__espaciador__"

        # Mostrar valor total o etiqueta personalizada solo si NO es espaciador
        if valor_total and not es_espaciador:
            # Si hay etiquetas_finales, usa esa etiqueta
            etiqueta_personal = None
            if etiquetas_finales is not None:
                try:
                    etiqueta_personal = etiquetas_finales.iloc[pos]
                except Exception:
                    etiqueta_personal = None
            texto_a_mostrar = f"{int(total_valor):,}"
            if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
                texto_a_mostrar = str(etiqueta_personal)
            if orientacion == 'horizontal':
                espacio = "\u00A0"
                texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
                if not quitar_capsula:
                    t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
            else:
                espacio = "\u00A0"
                texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
                if not quitar_capsula:
                    t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])

        # Mostrar porcentaje total al inicio de la barra si se solicita
        # ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
        if porcentaje_total_inicio:
            porcentaje = round((total_valor / total_general) * 100, 1)
            espacio = "\u00A0"
            etiqueta_actual = entidades_formateadas[pos]
            resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
            if resaltar:
                texto_capsula = f"{espacio*1}{etiqueta_actual}   {porcentaje}%{espacio*1}"
                bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
                color_texto = "white"
                if orientacion == 'horizontal':
                    ax.text(
                        -x_max*0.02, pos, texto_capsula,
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
                else:
                    ax.text(
                        pos, -x_max*0.02, texto_capsula,
                        ha='center', va='top',
                        rotation=90,  # <-- Esto rota la cápsula y el texto
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
            else:
                # Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
                if orientacion == 'horizontal':
                    ax.text(
                        0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )
                else:
                    ax.text(
                        pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='top',
                        rotation=90,  # <-- Esto rota el porcentaje solo
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )

    # Mostrar porcentaje total al lado derecho de la cápsula o del valor total
        if porcentaje_total:
            porcentaje = round((total_valor / total_general) * 100, 1)
            if orientacion == 'horizontal':
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.18 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        total_valor + desplazamiento, pos, f"{porcentaje}%",
                        ha='left', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
            else:
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.08 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        pos, total_valor + desplazamiento, f"{porcentaje}%",
                        ha='center', va='bottom',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
                
    # Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
    if porcentaje_total_inicio and resaltar_etiquetas is not None:
        entidades_formateadas = [
            "" if e in resaltar_etiquetas else e
            for e in entidades_formateadas
        ]

    # Configurar ejes y etiquetas
    if orientacion == 'horizontal':
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_yticks(posiciones)
        ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
                           fontweight=font_config['variable_y']['weight'],
                           fontfamily=font_config['family'])
        ax.invert_yaxis()
        ax.set_ylim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_xlim(y_limits)
        else:
            ax.set_xlim(0, 1.1 * x_max)
        ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_xticklabels(),
                 fontsize=font_config['variable_x']['size'],
                 fontweight=font_config['variable_x']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_x']['color'])
        if grillas:
            ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='y', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )
    else:
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_xticks(posiciones)
        ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
        ax.set_xlim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_ylim(y_limits)
        else:
            ax.set_ylim(0, 1.1 * x_max)
        ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_yticklabels(),
                 fontsize=font_config['variable_y']['size'],
                 fontweight=font_config['variable_y']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_y']['color'])
        if grillas:
            ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='x', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )    



    # Ajustar rotación de etiquetas especiales según la orientación
    if orientacion == 'vertical':
        for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
            if str(entidad) == "Previo":
                label.set_rotation(90)  # Cambia a vertical
                label.set_ha('center')
            elif str(entidad) == "...":
                label.set_rotation(0)
                label.set_ha('center')
    else:  # horizontal
        for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
            if str(entidad) == "...":
                label.set_rotation(90)
                label.set_va('center')
            elif str(entidad) == "Previo":
                label.set_rotation(0)
                label.set_va('center')

    if leyenda:
        ncol_leyenda = len(df_wide.columns)
        ax.legend(
            title=leyenda if isinstance(leyenda, str) else None,
            fontsize=font_config['leyenda']['size'],
            title_fontsize=font_config['leyenda']['size'],
            loc='upper center',
            bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
            frameon=False,
            ncol=ncol_leyenda
        )

        
    # Quitar bordes
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(False)

    plt.tight_layout()
    plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
    plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
    nombre_svg = f"{nombre_df}.svg"
    nombre_svg_limpio = f"{nombre_df}_scour.svg"
    nombre_svg_svgo = f"{nombre_df}_svgo.svg"

    try:
        limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
    except Exception as e:
        print("Error al limpiar con Scour:", e)

    try:
        limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
    except Exception as e:
        print("Error al limpiar con SVGO:", e)

    plt.show()

# EXPORTAR: barras_verticales_simples

barras_apiladas(df0, 
                nombre="barras_verticales_simples", 
                bar_height=0.9, 
                font='Montserrat',
                fontsize_barra=20,
                fontsize_valor_total=20,
                ordenar_por='etiqueta',
                orden='ascendente',
                valor_barra=False, 
                porcentaje_barra=False,
                porcentaje_total=True,
                orientacion='vertical',
                area_min=300
                )
Ver carpeta en GitHub
Barras horizontales simples

# EXPORTAR: barras_apiladas

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess

def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
    subprocess.run([
        'scour', '-i', archivo_entrada, '-o', archivo_salida,
        '--enable-viewboxing', '--enable-id-stripping',
        '--shorten-ids', '--remove-descriptive-elements'
    ], check=True)

def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
    subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)

def formato_fechas(fechas):
    fechas = pd.to_datetime(fechas)
    # Si todos los años son diferentes, mostrar solo el año
    if len(set(f.year for f in fechas)) == len(fechas):
        return [str(f.year) for f in fechas]
    else:
        return [f.strftime("%d-%m-%Y") for f in fechas]

def barras_apiladas(
    df_wide, 
    nombre=None,
    font='Montserrat',
    fontsize_barra=15, 
    fontsize_valor_total=20,
    bar_height=0.5, 
    aumenta_ancho_fig=0,
    aumenta_alto_fig=0,
    aumenta_sep_leyenda=0.0,
    valor_barra=False, 
    valor_total=True,
    porcentaje_barra=False, 
    porcentaje_total=False, 
    porcentaje_total_inicio=False,
    orientacion='horizontal',
    ordenar_por='valor',      # 'valor' o 'etiqueta'
    orden='descendente',       # 'ascendente' o 'descendente'
    quitar_capsula=False,
    area_min=0,
    espacio_inicio=0,  # Espacio para el porcentaje total al inicio de la barra 
    paleta_colores=None,
    agregar_datos=None,
    asignar_etiquetas=None,
    grillas=True,
    leyenda=None,
    union_izquierda=0,
    union_derecha=0,       
    sustituir_etiquetas=None,
    separar_por_total=0.0,
    y_limits=None,
    nombre_eje_x=None,
    nombre_eje_y=None,
    resaltar_etiquetas=None
):
    # Configuración de la fuente
    font_config = {
        'family': font,
        'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
        'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
        'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
        'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
    }

    plt.rcParams['svg.fonttype'] = 'none'
    font_dirs = [Path("../0_fonts")]
    font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
    for font_file in font_files:
        font_manager.fontManager.addfont(font_file)

    nombre_df = nombre or "barras_apiladas"
    if paleta_colores:
        colores_asignados = paleta_colores
    else:
        colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]

    if isinstance(df_wide, pd.DataFrame):
        df_wide = df_wide.copy()
        categorias_x = df_wide.columns[0]
        # Conversión automática de años enteros a fechas
        if (
            pd.api.types.is_integer_dtype(df_wide[categorias_x])
            and df_wide[categorias_x].between(1900, 2100).all()
        ):
            df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
        df_wide = df_wide.set_index(categorias_x)
    else:
        raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")

    # --- UNIÓN DE LAS PRIMERAS N BARRAS ---
    etiqueta_union_izq = "Unión"
    if union_izquierda > 1 and len(df_wide) >= union_izquierda:
        union = df_wide.iloc[:union_izquierda].sum()
        idxs = df_wide.index[:union_izquierda]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            pd.DataFrame([union], index=[etiqueta_union_izq]),
            df_wide.iloc[union_izquierda:]
        ])

    # --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
    etiqueta_union_der = "Unión"
    if union_derecha > 1 and len(df_wide) >= union_derecha:
        union = df_wide.iloc[-union_derecha:].sum()
        idxs = df_wide.index[-union_derecha:]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            df_wide.iloc[:-union_derecha],
            pd.DataFrame([union], index=[etiqueta_union_der])
        ])

    # --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
    df_wide = df_wide.copy()
    etiquetas_personalizadas = {}
    if agregar_datos is not None:
        filas_extra = []
        for item in agregar_datos:
            # Permite tuplas de 2 o 3 elementos
            if len(item) == 3:
                categoria, etiqueta, valor = item
                etiquetas_personalizadas[categoria] = etiqueta
            else:
                categoria, valor = item
                etiqueta = None
            if isinstance(valor, (list, tuple, np.ndarray)):
                fila = dict(zip(df_wide.columns, valor))
            else:
                fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
            filas_extra.append(pd.Series(fila, name=categoria))
        df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])

    # --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
    if agregar_datos is not None:
        # Determina el tipo de índice para la barra espaciadora
        if isinstance(df_wide.index[0], (int, np.integer, str)):
            categoria_espaciadora = "__espaciador__"
        else:
            # Obtener la última fecha de agregar_datos y la primera fecha del resto
            fechas_agregar = [d[0] for d in agregar_datos]
            fecha_max_agregar = max(fechas_agregar)
            fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
            if fechas_resto:
                fecha_min_resto = min(fechas_resto)
                # Poner la fecha espaciadora justo entre ambas
                categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
            else:
                # Si no hay resto, solo suma un día a la última de agregar_datos
                categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
        fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
        n_agregar = len(agregar_datos)
        df_wide = pd.concat([
            df_wide.iloc[:n_agregar],                  # barras de agregar_datos
            pd.DataFrame([fila_espaciadora]),          # espaciador
            df_wide.iloc[n_agregar:]                   # el resto
        ])

    etiquetas_finales = None
    if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
        etiquetas_finales = df_wide[asignar_etiquetas].copy()
        df_wide = df_wide.drop(columns=[asignar_etiquetas])        

    # Calcular totales
    suma_total = df_wide.sum(axis=1)
    total_general = suma_total.sum()

    # Definir x_max antes de dibujar las barras para evitar UnboundLocalError
    x_max = suma_total.max() * 1.15

    # ORDENAMIENTO
    if ordenar_por == 'valor':
        sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
    elif ordenar_por == 'etiqueta':
        # Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
        indices = list(df_wide.index)
        izq = []
        der = []
        centro = indices.copy()
        if union_izquierda > 1:
            izq = [etiqueta_union_izq]
            centro = [i for i in centro if i != etiqueta_union_izq]
        if union_derecha > 1:
            der = [etiqueta_union_der]
            centro = [i for i in centro if i != etiqueta_union_der]
        tipos = set(type(idx) for idx in centro)
        if len(tipos) > 1:
            centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
        else:
            centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
        sort_index = izq + centro_ordenado + der
    else:
        sort_index = df_wide.index  # sin ordenar si valor inválido

    df_wide = df_wide.loc[sort_index]
    suma_total = suma_total.loc[sort_index]


    entidades = df_wide.index.values

    # --- FORMATEO DE ETIQUETAS ---
    # Detectar índices de tipo fecha
    indices_fechas = [
        i for i, idx in enumerate(df_wide.index)
        if isinstance(idx, (pd.Timestamp, np.datetime64))
    ]
    # Formatear todas las fechas juntas si hay al menos una
    if indices_fechas:
        fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
    else:
        fechas_formateadas = []

    entidades_formateadas = []
    fecha_idx = 0
    for i, idx in enumerate(df_wide.index):
        if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
            entidades_formateadas.append(idx)
        elif isinstance(idx, (pd.Timestamp, np.datetime64)):
            entidades_formateadas.append(fechas_formateadas[fecha_idx])
            fecha_idx += 1
        else:
            entidades_formateadas.append(str(idx))

    es_fecha = any(
        isinstance(idx, (pd.Timestamp, np.datetime64))
        for idx in df_wide.index
        if not (
            (union_izquierda > 1 and idx == etiqueta_union_izq) or
            (union_derecha > 1 and idx == etiqueta_union_der)
        )
    )

    # --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
    if sustituir_etiquetas is not None:
        if len(sustituir_etiquetas) != len(entidades_formateadas):
            raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
        entidades_formateadas = list(sustituir_etiquetas)

    # Reemplaza etiquetas personalizadas si existen
    if etiquetas_personalizadas:
        for i, entidad in enumerate(df_wide.index):
            if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
                entidades_formateadas[i] = etiquetas_personalizadas[entidad]

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        # Detecta el valor exacto del espaciador
        if es_fecha:
            # El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
            fechas_agregar = [d[0] for d in agregar_datos]
            fechas_todas = list(df_wide.index)
            fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
            anio_espaciador = None
            for idx in fechas_resto:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[idx] == 0).all():
                    anio_espaciador = idx
                    break
            for i, entidad in enumerate(df_wide.index):
                if entidad == anio_espaciador:
                    entidades_formateadas[i] = "..."
        else:
            for i, entidad in enumerate(entidades_formateadas):
                if entidad == "__espaciador__":
                    entidades_formateadas[i] = "..."


    valores = [df_wide[col].values for col in df_wide.columns]
    categorias = df_wide.columns
    posiciones = np.arange(len(entidades))

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        for i, entidad in enumerate(entidades_formateadas):
            if entidad == "__espaciador__":
                entidades_formateadas[i] = "..."

    # --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
    if orientacion == 'vertical':
        longitudes = []
        for total_valor in suma_total:
            espacio = "\u00A0"
            texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
            longitudes.append(len(texto_capsula))
        if longitudes:
            moda_capsula_len = mode(longitudes)
        else:
            moda_capsula_len = 10
        extra_width = moda_capsula_len * 0.1
        base_width = max(12, len(entidades)*extra_width)
        fig_width = base_width + aumenta_ancho_fig
        fig_height = 8 + aumenta_alto_fig
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    else:
        fig_width = 12 + aumenta_ancho_fig          
        fig_height = 15 + aumenta_alto_fig                 
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    # ---------------------------------------------------------------

    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    # Dibujar barras apiladas
    for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
        left = 0
        for i, valor in enumerate(valores):
            color = colores_asignados[i % len(colores_asignados)]
            label = categorias[i] if pos == 0 else None
            if orientacion == 'horizontal':
                if valor[pos] > 0:
                    ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                        if porcentaje_barra:
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                        ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
                                fontsize=font_config['valor_porcentaje_barra']['size'],
                                fontfamily=font_config['family'],
                                fontweight=font_config['valor_porcentaje_barra']['weight'],
                                color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]
            else:
                if valor[pos] > 0:
                    ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        if orientacion == 'vertical' and valor_barra and porcentaje_barra:
                            # Valor arriba, porcentaje abajo
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
                                    va='bottom', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                            ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
                                    va='top', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size']-2,
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                        else:
                            texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                            if porcentaje_barra:
                                porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                                texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                            ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            es_espaciador = entidad == "__espaciador__"

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            # Detecta si el índice es el espaciador
            if es_fecha:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[entidad] == 0).all():
                    es_espaciador = True
            else:
                es_espaciador = entidad == "__espaciador__"

        # Mostrar valor total o etiqueta personalizada solo si NO es espaciador
        if valor_total and not es_espaciador:
            # Si hay etiquetas_finales, usa esa etiqueta
            etiqueta_personal = None
            if etiquetas_finales is not None:
                try:
                    etiqueta_personal = etiquetas_finales.iloc[pos]
                except Exception:
                    etiqueta_personal = None
            texto_a_mostrar = f"{int(total_valor):,}"
            if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
                texto_a_mostrar = str(etiqueta_personal)
            if orientacion == 'horizontal':
                espacio = "\u00A0"
                texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
                if not quitar_capsula:
                    t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
            else:
                espacio = "\u00A0"
                texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
                if not quitar_capsula:
                    t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])

        # Mostrar porcentaje total al inicio de la barra si se solicita
        # ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
        if porcentaje_total_inicio:
            porcentaje = round((total_valor / total_general) * 100, 1)
            espacio = "\u00A0"
            etiqueta_actual = entidades_formateadas[pos]
            resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
            if resaltar:
                texto_capsula = f"{espacio*1}{etiqueta_actual}   {porcentaje}%{espacio*1}"
                bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
                color_texto = "white"
                if orientacion == 'horizontal':
                    ax.text(
                        -x_max*0.02, pos, texto_capsula,
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
                else:
                    ax.text(
                        pos, -x_max*0.02, texto_capsula,
                        ha='center', va='top',
                        rotation=90,  # <-- Esto rota la cápsula y el texto
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
            else:
                # Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
                if orientacion == 'horizontal':
                    ax.text(
                        0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )
                else:
                    ax.text(
                        pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='top',
                        rotation=90,  # <-- Esto rota el porcentaje solo
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )

    # Mostrar porcentaje total al lado derecho de la cápsula o del valor total
        if porcentaje_total:
            porcentaje = round((total_valor / total_general) * 100, 1)
            if orientacion == 'horizontal':
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.18 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        total_valor + desplazamiento, pos, f"{porcentaje}%",
                        ha='left', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
            else:
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.08 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        pos, total_valor + desplazamiento, f"{porcentaje}%",
                        ha='center', va='bottom',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
                
    # Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
    if porcentaje_total_inicio and resaltar_etiquetas is not None:
        entidades_formateadas = [
            "" if e in resaltar_etiquetas else e
            for e in entidades_formateadas
        ]

    # Configurar ejes y etiquetas
    if orientacion == 'horizontal':
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_yticks(posiciones)
        ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
                           fontweight=font_config['variable_y']['weight'],
                           fontfamily=font_config['family'])
        ax.invert_yaxis()
        ax.set_ylim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_xlim(y_limits)
        else:
            ax.set_xlim(0, 1.1 * x_max)
        ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_xticklabels(),
                 fontsize=font_config['variable_x']['size'],
                 fontweight=font_config['variable_x']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_x']['color'])
        if grillas:
            ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='y', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )
    else:
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_xticks(posiciones)
        ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
        ax.set_xlim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_ylim(y_limits)
        else:
            ax.set_ylim(0, 1.1 * x_max)
        ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_yticklabels(),
                 fontsize=font_config['variable_y']['size'],
                 fontweight=font_config['variable_y']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_y']['color'])
        if grillas:
            ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='x', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )    



    # Ajustar rotación de etiquetas especiales según la orientación
    if orientacion == 'vertical':
        for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
            if str(entidad) == "Previo":
                label.set_rotation(90)  # Cambia a vertical
                label.set_ha('center')
            elif str(entidad) == "...":
                label.set_rotation(0)
                label.set_ha('center')
    else:  # horizontal
        for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
            if str(entidad) == "...":
                label.set_rotation(90)
                label.set_va('center')
            elif str(entidad) == "Previo":
                label.set_rotation(0)
                label.set_va('center')

    if leyenda:
        ncol_leyenda = len(df_wide.columns)
        ax.legend(
            title=leyenda if isinstance(leyenda, str) else None,
            fontsize=font_config['leyenda']['size'],
            title_fontsize=font_config['leyenda']['size'],
            loc='upper center',
            bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
            frameon=False,
            ncol=ncol_leyenda
        )

        
    # Quitar bordes
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(False)

    plt.tight_layout()
    plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
    plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
    nombre_svg = f"{nombre_df}.svg"
    nombre_svg_limpio = f"{nombre_df}_scour.svg"
    nombre_svg_svgo = f"{nombre_df}_svgo.svg"

    try:
        limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
    except Exception as e:
        print("Error al limpiar con Scour:", e)

    try:
        limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
    except Exception as e:
        print("Error al limpiar con SVGO:", e)

    plt.show()
Ver carpeta en GitHub
Barra previa

# EXPORTAR: barras_apiladas

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess

def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
    subprocess.run([
        'scour', '-i', archivo_entrada, '-o', archivo_salida,
        '--enable-viewboxing', '--enable-id-stripping',
        '--shorten-ids', '--remove-descriptive-elements'
    ], check=True)

def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
    subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)

def formato_fechas(fechas):
    fechas = pd.to_datetime(fechas)
    # Si todos los años son diferentes, mostrar solo el año
    if len(set(f.year for f in fechas)) == len(fechas):
        return [str(f.year) for f in fechas]
    else:
        return [f.strftime("%d-%m-%Y") for f in fechas]

def barras_apiladas(
    df_wide, 
    nombre=None,
    font='Montserrat',
    fontsize_barra=15, 
    fontsize_valor_total=20,
    bar_height=0.5, 
    aumenta_ancho_fig=0,
    aumenta_alto_fig=0,
    aumenta_sep_leyenda=0.0,
    valor_barra=False, 
    valor_total=True,
    porcentaje_barra=False, 
    porcentaje_total=False, 
    porcentaje_total_inicio=False,
    orientacion='horizontal',
    ordenar_por='valor',      # 'valor' o 'etiqueta'
    orden='descendente',       # 'ascendente' o 'descendente'
    quitar_capsula=False,
    area_min=0,
    espacio_inicio=0,  # Espacio para el porcentaje total al inicio de la barra 
    paleta_colores=None,
    agregar_datos=None,
    asignar_etiquetas=None,
    grillas=True,
    leyenda=None,
    union_izquierda=0,
    union_derecha=0,       
    sustituir_etiquetas=None,
    separar_por_total=0.0,
    y_limits=None,
    nombre_eje_x=None,
    nombre_eje_y=None,
    resaltar_etiquetas=None
):
    # Configuración de la fuente
    font_config = {
        'family': font,
        'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
        'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
        'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
        'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
    }

    plt.rcParams['svg.fonttype'] = 'none'
    font_dirs = [Path("../0_fonts")]
    font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
    for font_file in font_files:
        font_manager.fontManager.addfont(font_file)

    nombre_df = nombre or "barras_apiladas"
    if paleta_colores:
        colores_asignados = paleta_colores
    else:
        colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]

    if isinstance(df_wide, pd.DataFrame):
        df_wide = df_wide.copy()
        categorias_x = df_wide.columns[0]
        # Conversión automática de años enteros a fechas
        if (
            pd.api.types.is_integer_dtype(df_wide[categorias_x])
            and df_wide[categorias_x].between(1900, 2100).all()
        ):
            df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
        df_wide = df_wide.set_index(categorias_x)
    else:
        raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")

    # --- UNIÓN DE LAS PRIMERAS N BARRAS ---
    etiqueta_union_izq = "Unión"
    if union_izquierda > 1 and len(df_wide) >= union_izquierda:
        union = df_wide.iloc[:union_izquierda].sum()
        idxs = df_wide.index[:union_izquierda]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            pd.DataFrame([union], index=[etiqueta_union_izq]),
            df_wide.iloc[union_izquierda:]
        ])

    # --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
    etiqueta_union_der = "Unión"
    if union_derecha > 1 and len(df_wide) >= union_derecha:
        union = df_wide.iloc[-union_derecha:].sum()
        idxs = df_wide.index[-union_derecha:]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            df_wide.iloc[:-union_derecha],
            pd.DataFrame([union], index=[etiqueta_union_der])
        ])

    # --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
    df_wide = df_wide.copy()
    etiquetas_personalizadas = {}
    if agregar_datos is not None:
        filas_extra = []
        for item in agregar_datos:
            # Permite tuplas de 2 o 3 elementos
            if len(item) == 3:
                categoria, etiqueta, valor = item
                etiquetas_personalizadas[categoria] = etiqueta
            else:
                categoria, valor = item
                etiqueta = None
            if isinstance(valor, (list, tuple, np.ndarray)):
                fila = dict(zip(df_wide.columns, valor))
            else:
                fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
            filas_extra.append(pd.Series(fila, name=categoria))
        df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])

    # --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
    if agregar_datos is not None:
        # Determina el tipo de índice para la barra espaciadora
        if isinstance(df_wide.index[0], (int, np.integer, str)):
            categoria_espaciadora = "__espaciador__"
        else:
            # Obtener la última fecha de agregar_datos y la primera fecha del resto
            fechas_agregar = [d[0] for d in agregar_datos]
            fecha_max_agregar = max(fechas_agregar)
            fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
            if fechas_resto:
                fecha_min_resto = min(fechas_resto)
                # Poner la fecha espaciadora justo entre ambas
                categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
            else:
                # Si no hay resto, solo suma un día a la última de agregar_datos
                categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
        fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
        n_agregar = len(agregar_datos)
        df_wide = pd.concat([
            df_wide.iloc[:n_agregar],                  # barras de agregar_datos
            pd.DataFrame([fila_espaciadora]),          # espaciador
            df_wide.iloc[n_agregar:]                   # el resto
        ])

    etiquetas_finales = None
    if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
        etiquetas_finales = df_wide[asignar_etiquetas].copy()
        df_wide = df_wide.drop(columns=[asignar_etiquetas])        

    # Calcular totales
    suma_total = df_wide.sum(axis=1)
    total_general = suma_total.sum()

    # Definir x_max antes de dibujar las barras para evitar UnboundLocalError
    x_max = suma_total.max() * 1.15

    # ORDENAMIENTO
    if ordenar_por == 'valor':
        sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
    elif ordenar_por == 'etiqueta':
        # Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
        indices = list(df_wide.index)
        izq = []
        der = []
        centro = indices.copy()
        if union_izquierda > 1:
            izq = [etiqueta_union_izq]
            centro = [i for i in centro if i != etiqueta_union_izq]
        if union_derecha > 1:
            der = [etiqueta_union_der]
            centro = [i for i in centro if i != etiqueta_union_der]
        tipos = set(type(idx) for idx in centro)
        if len(tipos) > 1:
            centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
        else:
            centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
        sort_index = izq + centro_ordenado + der
    else:
        sort_index = df_wide.index  # sin ordenar si valor inválido

    df_wide = df_wide.loc[sort_index]
    suma_total = suma_total.loc[sort_index]


    entidades = df_wide.index.values

    # --- FORMATEO DE ETIQUETAS ---
    # Detectar índices de tipo fecha
    indices_fechas = [
        i for i, idx in enumerate(df_wide.index)
        if isinstance(idx, (pd.Timestamp, np.datetime64))
    ]
    # Formatear todas las fechas juntas si hay al menos una
    if indices_fechas:
        fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
    else:
        fechas_formateadas = []

    entidades_formateadas = []
    fecha_idx = 0
    for i, idx in enumerate(df_wide.index):
        if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
            entidades_formateadas.append(idx)
        elif isinstance(idx, (pd.Timestamp, np.datetime64)):
            entidades_formateadas.append(fechas_formateadas[fecha_idx])
            fecha_idx += 1
        else:
            entidades_formateadas.append(str(idx))

    es_fecha = any(
        isinstance(idx, (pd.Timestamp, np.datetime64))
        for idx in df_wide.index
        if not (
            (union_izquierda > 1 and idx == etiqueta_union_izq) or
            (union_derecha > 1 and idx == etiqueta_union_der)
        )
    )

    # --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
    if sustituir_etiquetas is not None:
        if len(sustituir_etiquetas) != len(entidades_formateadas):
            raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
        entidades_formateadas = list(sustituir_etiquetas)

    # Reemplaza etiquetas personalizadas si existen
    if etiquetas_personalizadas:
        for i, entidad in enumerate(df_wide.index):
            if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
                entidades_formateadas[i] = etiquetas_personalizadas[entidad]

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        # Detecta el valor exacto del espaciador
        if es_fecha:
            # El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
            fechas_agregar = [d[0] for d in agregar_datos]
            fechas_todas = list(df_wide.index)
            fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
            anio_espaciador = None
            for idx in fechas_resto:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[idx] == 0).all():
                    anio_espaciador = idx
                    break
            for i, entidad in enumerate(df_wide.index):
                if entidad == anio_espaciador:
                    entidades_formateadas[i] = "..."
        else:
            for i, entidad in enumerate(entidades_formateadas):
                if entidad == "__espaciador__":
                    entidades_formateadas[i] = "..."


    valores = [df_wide[col].values for col in df_wide.columns]
    categorias = df_wide.columns
    posiciones = np.arange(len(entidades))

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        for i, entidad in enumerate(entidades_formateadas):
            if entidad == "__espaciador__":
                entidades_formateadas[i] = "..."

    # --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
    if orientacion == 'vertical':
        longitudes = []
        for total_valor in suma_total:
            espacio = "\u00A0"
            texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
            longitudes.append(len(texto_capsula))
        if longitudes:
            moda_capsula_len = mode(longitudes)
        else:
            moda_capsula_len = 10
        extra_width = moda_capsula_len * 0.1
        base_width = max(12, len(entidades)*extra_width)
        fig_width = base_width + aumenta_ancho_fig
        fig_height = 8 + aumenta_alto_fig
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    else:
        fig_width = 12 + aumenta_ancho_fig          
        fig_height = 15 + aumenta_alto_fig                 
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    # ---------------------------------------------------------------

    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    # Dibujar barras apiladas
    for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
        left = 0
        for i, valor in enumerate(valores):
            color = colores_asignados[i % len(colores_asignados)]
            label = categorias[i] if pos == 0 else None
            if orientacion == 'horizontal':
                if valor[pos] > 0:
                    ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                        if porcentaje_barra:
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                        ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
                                fontsize=font_config['valor_porcentaje_barra']['size'],
                                fontfamily=font_config['family'],
                                fontweight=font_config['valor_porcentaje_barra']['weight'],
                                color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]
            else:
                if valor[pos] > 0:
                    ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        if orientacion == 'vertical' and valor_barra and porcentaje_barra:
                            # Valor arriba, porcentaje abajo
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
                                    va='bottom', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                            ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
                                    va='top', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size']-2,
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                        else:
                            texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                            if porcentaje_barra:
                                porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                                texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                            ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            es_espaciador = entidad == "__espaciador__"

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            # Detecta si el índice es el espaciador
            if es_fecha:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[entidad] == 0).all():
                    es_espaciador = True
            else:
                es_espaciador = entidad == "__espaciador__"

        # Mostrar valor total o etiqueta personalizada solo si NO es espaciador
        if valor_total and not es_espaciador:
            # Si hay etiquetas_finales, usa esa etiqueta
            etiqueta_personal = None
            if etiquetas_finales is not None:
                try:
                    etiqueta_personal = etiquetas_finales.iloc[pos]
                except Exception:
                    etiqueta_personal = None
            texto_a_mostrar = f"{int(total_valor):,}"
            if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
                texto_a_mostrar = str(etiqueta_personal)
            if orientacion == 'horizontal':
                espacio = "\u00A0"
                texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
                if not quitar_capsula:
                    t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
            else:
                espacio = "\u00A0"
                texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
                if not quitar_capsula:
                    t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])

        # Mostrar porcentaje total al inicio de la barra si se solicita
        # ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
        if porcentaje_total_inicio:
            porcentaje = round((total_valor / total_general) * 100, 1)
            espacio = "\u00A0"
            etiqueta_actual = entidades_formateadas[pos]
            resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
            if resaltar:
                texto_capsula = f"{espacio*1}{etiqueta_actual}   {porcentaje}%{espacio*1}"
                bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
                color_texto = "white"
                if orientacion == 'horizontal':
                    ax.text(
                        -x_max*0.02, pos, texto_capsula,
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
                else:
                    ax.text(
                        pos, -x_max*0.02, texto_capsula,
                        ha='center', va='top',
                        rotation=90,  # <-- Esto rota la cápsula y el texto
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
            else:
                # Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
                if orientacion == 'horizontal':
                    ax.text(
                        0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )
                else:
                    ax.text(
                        pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='top',
                        rotation=90,  # <-- Esto rota el porcentaje solo
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )

    # Mostrar porcentaje total al lado derecho de la cápsula o del valor total
        if porcentaje_total:
            porcentaje = round((total_valor / total_general) * 100, 1)
            if orientacion == 'horizontal':
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.18 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        total_valor + desplazamiento, pos, f"{porcentaje}%",
                        ha='left', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
            else:
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.08 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        pos, total_valor + desplazamiento, f"{porcentaje}%",
                        ha='center', va='bottom',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
                
    # Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
    if porcentaje_total_inicio and resaltar_etiquetas is not None:
        entidades_formateadas = [
            "" if e in resaltar_etiquetas else e
            for e in entidades_formateadas
        ]

    # Configurar ejes y etiquetas
    if orientacion == 'horizontal':
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_yticks(posiciones)
        ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
                           fontweight=font_config['variable_y']['weight'],
                           fontfamily=font_config['family'])
        ax.invert_yaxis()
        ax.set_ylim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_xlim(y_limits)
        else:
            ax.set_xlim(0, 1.1 * x_max)
        ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_xticklabels(),
                 fontsize=font_config['variable_x']['size'],
                 fontweight=font_config['variable_x']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_x']['color'])
        if grillas:
            ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='y', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )
    else:
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_xticks(posiciones)
        ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
        ax.set_xlim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_ylim(y_limits)
        else:
            ax.set_ylim(0, 1.1 * x_max)
        ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_yticklabels(),
                 fontsize=font_config['variable_y']['size'],
                 fontweight=font_config['variable_y']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_y']['color'])
        if grillas:
            ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='x', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )    



    # Ajustar rotación de etiquetas especiales según la orientación
    if orientacion == 'vertical':
        for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
            if str(entidad) == "Previo":
                label.set_rotation(90)  # Cambia a vertical
                label.set_ha('center')
            elif str(entidad) == "...":
                label.set_rotation(0)
                label.set_ha('center')
    else:  # horizontal
        for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
            if str(entidad) == "...":
                label.set_rotation(90)
                label.set_va('center')
            elif str(entidad) == "Previo":
                label.set_rotation(0)
                label.set_va('center')

    if leyenda:
        ncol_leyenda = len(df_wide.columns)
        ax.legend(
            title=leyenda if isinstance(leyenda, str) else None,
            fontsize=font_config['leyenda']['size'],
            title_fontsize=font_config['leyenda']['size'],
            loc='upper center',
            bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
            frameon=False,
            ncol=ncol_leyenda
        )

        
    # Quitar bordes
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(False)

    plt.tight_layout()
    plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
    plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
    nombre_svg = f"{nombre_df}.svg"
    nombre_svg_limpio = f"{nombre_df}_scour.svg"
    nombre_svg_svgo = f"{nombre_df}_svgo.svg"

    try:
        limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
    except Exception as e:
        print("Error al limpiar con Scour:", e)

    try:
        limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
    except Exception as e:
        print("Error al limpiar con SVGO:", e)

    plt.show()

# EXPORTAR: barra_previa

barras_apiladas(df1f, 
                nombre="barra_previa", 
                bar_height=0.9, 
                font='Montserrat',
                fontsize_valor_total=30,
                ordenar_por='etiqueta',
                orden='ascendente',
                valor_barra=False, 
                porcentaje_barra=False,
                porcentaje_total=False,
                orientacion='vertical',
                quitar_capsula=True,
                agregar_datos=[(pd.Timestamp(2005, 1, 1), "Previo", (192, 313, "38%"))],
                asignar_etiquetas='porcentaje',
                grillas=False,
                paleta_colores=["#10302C", "#E6E6E6"]
                )
Ver carpeta en GitHub
Barras de tendencia

# EXPORTAR: barras_apiladas

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from pathlib import Path
import matplotlib.ticker as mticker
from matplotlib.ticker import FuncFormatter
from statistics import mode
import subprocess

def limpiar_svg_con_scour(archivo_entrada, archivo_salida):
    subprocess.run([
        'scour', '-i', archivo_entrada, '-o', archivo_salida,
        '--enable-viewboxing', '--enable-id-stripping',
        '--shorten-ids', '--remove-descriptive-elements'
    ], check=True)

def limpiar_svg_con_svgo(archivo_entrada, archivo_salida):
    subprocess.run(['svgo', archivo_entrada, '-o', archivo_salida], check=True)

def formato_fechas(fechas):
    fechas = pd.to_datetime(fechas)
    # Si todos los años son diferentes, mostrar solo el año
    if len(set(f.year for f in fechas)) == len(fechas):
        return [str(f.year) for f in fechas]
    else:
        return [f.strftime("%d-%m-%Y") for f in fechas]

def barras_apiladas(
    df_wide, 
    nombre=None,
    font='Montserrat',
    fontsize_barra=15, 
    fontsize_valor_total=20,
    bar_height=0.5, 
    aumenta_ancho_fig=0,
    aumenta_alto_fig=0,
    aumenta_sep_leyenda=0.0,
    valor_barra=False, 
    valor_total=True,
    porcentaje_barra=False, 
    porcentaje_total=False, 
    porcentaje_total_inicio=False,
    orientacion='horizontal',
    ordenar_por='valor',      # 'valor' o 'etiqueta'
    orden='descendente',       # 'ascendente' o 'descendente'
    quitar_capsula=False,
    area_min=0,
    espacio_inicio=0,  # Espacio para el porcentaje total al inicio de la barra 
    paleta_colores=None,
    agregar_datos=None,
    asignar_etiquetas=None,
    grillas=True,
    leyenda=None,
    union_izquierda=0,
    union_derecha=0,       
    sustituir_etiquetas=None,
    separar_por_total=0.0,
    y_limits=None,
    nombre_eje_x=None,
    nombre_eje_y=None,
    resaltar_etiquetas=None
):
    # Configuración de la fuente
    font_config = {
        'family': font,
        'variable_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'variable_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_x': {'size': 25, 'weight': 'medium', 'color': '#000000'},
        'nombre_eje_y': {'size': 22, 'weight': 'medium', 'color': '#000000'},
        'valor_capsula': {'size': fontsize_valor_total, 'weight': 'bold', 'color': '#10302C'},
        'valor_porcentaje_barra': {'size': fontsize_barra, 'weight': 'medium', 'color': '#ffffff'},
        'porcentaje_total': {'size': 22, 'weight': 'semibold', 'color': '#4C6A67'},
        'leyenda': {'size': 24, 'weight': 'medium', 'color': '#767676'}
    }

    plt.rcParams['svg.fonttype'] = 'none'
    font_dirs = [Path("../0_fonts")]
    font_files = font_manager.findSystemFonts(fontpaths=font_dirs)
    for font_file in font_files:
        font_manager.fontManager.addfont(font_file)

    nombre_df = nombre or "barras_apiladas"
    if paleta_colores:
        colores_asignados = paleta_colores
    else:
        colores_asignados = ["#10302C", "#4C6A67", "#8FA8A6", "#A3C9A8"]

    if isinstance(df_wide, pd.DataFrame):
        df_wide = df_wide.copy()
        categorias_x = df_wide.columns[0]
        # Conversión automática de años enteros a fechas
        if (
            pd.api.types.is_integer_dtype(df_wide[categorias_x])
            and df_wide[categorias_x].between(1900, 2100).all()
        ):
            df_wide[categorias_x] = df_wide[categorias_x].apply(lambda x: pd.Timestamp(x, 1, 1))
        df_wide = df_wide.set_index(categorias_x)
    else:
        raise ValueError("El argumento df_wide debe ser un DataFrame de pandas.")

    # --- UNIÓN DE LAS PRIMERAS N BARRAS ---
    etiqueta_union_izq = "Unión"
    if union_izquierda > 1 and len(df_wide) >= union_izquierda:
        union = df_wide.iloc[:union_izquierda].sum()
        idxs = df_wide.index[:union_izquierda]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_izq = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_izq = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            pd.DataFrame([union], index=[etiqueta_union_izq]),
            df_wide.iloc[union_izquierda:]
        ])

    # --- UNIÓN DE LAS ÚLTIMAS N BARRAS ---
    etiqueta_union_der = "Unión"
    if union_derecha > 1 and len(df_wide) >= union_derecha:
        union = df_wide.iloc[-union_derecha:].sum()
        idxs = df_wide.index[-union_derecha:]
        if np.issubdtype(type(idxs[0]), np.datetime64) or isinstance(idxs[0], pd.Timestamp):
            etiqueta_union_der = f"{idxs[0].year}-{idxs[-1].year}"
        elif isinstance(idxs[0], (int, np.integer, float)):
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        else:
            etiqueta_union_der = f"{idxs[0]}-{idxs[-1]}"
        df_wide = pd.concat([
            df_wide.iloc[:-union_derecha],
            pd.DataFrame([union], index=[etiqueta_union_der])
        ])

    # --- AGREGAR DATOS EXTRA SI agregar_datos ESTÁ DEFINIDO ---
    df_wide = df_wide.copy()
    etiquetas_personalizadas = {}
    if agregar_datos is not None:
        filas_extra = []
        for item in agregar_datos:
            # Permite tuplas de 2 o 3 elementos
            if len(item) == 3:
                categoria, etiqueta, valor = item
                etiquetas_personalizadas[categoria] = etiqueta
            else:
                categoria, valor = item
                etiqueta = None
            if isinstance(valor, (list, tuple, np.ndarray)):
                fila = dict(zip(df_wide.columns, valor))
            else:
                fila = {col: valor if i == 0 else 0 for i, col in enumerate(df_wide.columns)}
            filas_extra.append(pd.Series(fila, name=categoria))
        df_wide = pd.concat([pd.DataFrame(filas_extra), df_wide])

    # --- AGREGAR UNA FILA ESPACIADORA ENTRE agregar_datos Y EL RESTO ---
    if agregar_datos is not None:
        # Determina el tipo de índice para la barra espaciadora
        if isinstance(df_wide.index[0], (int, np.integer, str)):
            categoria_espaciadora = "__espaciador__"
        else:
            # Obtener la última fecha de agregar_datos y la primera fecha del resto
            fechas_agregar = [d[0] for d in agregar_datos]
            fecha_max_agregar = max(fechas_agregar)
            fechas_resto = [idx for idx in df_wide.index[len(agregar_datos):] if isinstance(idx, pd.Timestamp)]
            if fechas_resto:
                fecha_min_resto = min(fechas_resto)
                # Poner la fecha espaciadora justo entre ambas
                categoria_espaciadora = fecha_max_agregar + (fecha_min_resto - fecha_max_agregar) / 2
            else:
                # Si no hay resto, solo suma un día a la última de agregar_datos
                categoria_espaciadora = fecha_max_agregar + pd.Timedelta(days=1)
        fila_espaciadora = pd.Series({col: 0 for col in df_wide.columns}, name=categoria_espaciadora)
        n_agregar = len(agregar_datos)
        df_wide = pd.concat([
            df_wide.iloc[:n_agregar],                  # barras de agregar_datos
            pd.DataFrame([fila_espaciadora]),          # espaciador
            df_wide.iloc[n_agregar:]                   # el resto
        ])

    etiquetas_finales = None
    if asignar_etiquetas is not None and asignar_etiquetas in df_wide.columns:
        etiquetas_finales = df_wide[asignar_etiquetas].copy()
        df_wide = df_wide.drop(columns=[asignar_etiquetas])        

    # Calcular totales
    suma_total = df_wide.sum(axis=1)
    total_general = suma_total.sum()

    # Definir x_max antes de dibujar las barras para evitar UnboundLocalError
    x_max = suma_total.max() * 1.15

    # ORDENAMIENTO
    if ordenar_por == 'valor':
        sort_index = suma_total.sort_values(ascending=(orden == 'ascendente')).index
    elif ordenar_por == 'etiqueta':
        # Mantener la barra unida a la izquierda y/o derecha, ordenar el resto
        indices = list(df_wide.index)
        izq = []
        der = []
        centro = indices.copy()
        if union_izquierda > 1:
            izq = [etiqueta_union_izq]
            centro = [i for i in centro if i != etiqueta_union_izq]
        if union_derecha > 1:
            der = [etiqueta_union_der]
            centro = [i for i in centro if i != etiqueta_union_der]
        tipos = set(type(idx) for idx in centro)
        if len(tipos) > 1:
            centro_ordenado = sorted(centro, key=lambda x: str(x), reverse=(orden == 'descendente'))
        else:
            centro_ordenado = sorted(centro, reverse=(orden == 'descendente'))
        sort_index = izq + centro_ordenado + der
    else:
        sort_index = df_wide.index  # sin ordenar si valor inválido

    df_wide = df_wide.loc[sort_index]
    suma_total = suma_total.loc[sort_index]


    entidades = df_wide.index.values

    # --- FORMATEO DE ETIQUETAS ---
    # Detectar índices de tipo fecha
    indices_fechas = [
        i for i, idx in enumerate(df_wide.index)
        if isinstance(idx, (pd.Timestamp, np.datetime64))
    ]
    # Formatear todas las fechas juntas si hay al menos una
    if indices_fechas:
        fechas_formateadas = formato_fechas([df_wide.index[i] for i in indices_fechas])
    else:
        fechas_formateadas = []

    entidades_formateadas = []
    fecha_idx = 0
    for i, idx in enumerate(df_wide.index):
        if (union_izquierda > 1 and idx == etiqueta_union_izq) or (union_derecha > 1 and idx == etiqueta_union_der):
            entidades_formateadas.append(idx)
        elif isinstance(idx, (pd.Timestamp, np.datetime64)):
            entidades_formateadas.append(fechas_formateadas[fecha_idx])
            fecha_idx += 1
        else:
            entidades_formateadas.append(str(idx))

    es_fecha = any(
        isinstance(idx, (pd.Timestamp, np.datetime64))
        for idx in df_wide.index
        if not (
            (union_izquierda > 1 and idx == etiqueta_union_izq) or
            (union_derecha > 1 and idx == etiqueta_union_der)
        )
    )

    # --- SUSTITUIR ETIQUETAS SI SE PROPORCIONA LA LISTA ---
    if sustituir_etiquetas is not None:
        if len(sustituir_etiquetas) != len(entidades_formateadas):
            raise ValueError("La longitud de 'sustituir_etiquetas' debe coincidir con el número de barras.")
        entidades_formateadas = list(sustituir_etiquetas)

    # Reemplaza etiquetas personalizadas si existen
    if etiquetas_personalizadas:
        for i, entidad in enumerate(df_wide.index):
            if entidad in etiquetas_personalizadas and etiquetas_personalizadas[entidad] is not None:
                entidades_formateadas[i] = etiquetas_personalizadas[entidad]

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        # Detecta el valor exacto del espaciador
        if es_fecha:
            # El espaciador es la única fecha que no está en los datos originales ni en agregar_datos
            fechas_agregar = [d[0] for d in agregar_datos]
            fechas_todas = list(df_wide.index)
            fechas_resto = [idx for idx in fechas_todas if idx not in fechas_agregar]
            anio_espaciador = None
            for idx in fechas_resto:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[idx] == 0).all():
                    anio_espaciador = idx
                    break
            for i, entidad in enumerate(df_wide.index):
                if entidad == anio_espaciador:
                    entidades_formateadas[i] = "..."
        else:
            for i, entidad in enumerate(entidades_formateadas):
                if entidad == "__espaciador__":
                    entidades_formateadas[i] = "..."


    valores = [df_wide[col].values for col in df_wide.columns]
    categorias = df_wide.columns
    posiciones = np.arange(len(entidades))

    # Si hay espaciador, pon etiqueta "..."
    if agregar_datos is not None:
        for i, entidad in enumerate(entidades_formateadas):
            if entidad == "__espaciador__":
                entidades_formateadas[i] = "..."

    # --- Ajuste del ancho de la figura según el ancho de las cápsulas ---
    if orientacion == 'vertical':
        longitudes = []
        for total_valor in suma_total:
            espacio = "\u00A0"
            texto_capsula = f"{espacio*4}{int(total_valor):,}{espacio*4}"
            longitudes.append(len(texto_capsula))
        if longitudes:
            moda_capsula_len = mode(longitudes)
        else:
            moda_capsula_len = 10
        extra_width = moda_capsula_len * 0.1
        base_width = max(12, len(entidades)*extra_width)
        fig_width = base_width + aumenta_ancho_fig
        fig_height = 8 + aumenta_alto_fig
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    else:
        fig_width = 12 + aumenta_ancho_fig          
        fig_height = 15 + aumenta_alto_fig                 
        fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=300)
    # ---------------------------------------------------------------

    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')

    # Dibujar barras apiladas
    for pos, entidad, total_valor in zip(posiciones, entidades, suma_total):
        left = 0
        for i, valor in enumerate(valores):
            color = colores_asignados[i % len(colores_asignados)]
            label = categorias[i] if pos == 0 else None
            if orientacion == 'horizontal':
                if valor[pos] > 0:
                    ax.barh(pos, valor[pos], height=bar_height, left=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                        if porcentaje_barra:
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                        ax.text(left + valor[pos] / 2, pos, texto, va='center', ha='center',
                                fontsize=font_config['valor_porcentaje_barra']['size'],
                                fontfamily=font_config['family'],
                                fontweight=font_config['valor_porcentaje_barra']['weight'],
                                color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]
            else:
                if valor[pos] > 0:
                    ax.bar(pos, valor[pos], width=bar_height, bottom=left, color=color, edgecolor='none', zorder=2, label=label)
                    area_barra = valor[pos] * bar_height
                    if (porcentaje_barra or valor_barra) and area_barra >= area_min:
                        if orientacion == 'vertical' and valor_barra and porcentaje_barra:
                            # Valor arriba, porcentaje abajo
                            porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                            ax.text(pos, left + valor[pos] / 2 + 0.8 * bar_height, f"{valor[pos]:,.0f}",
                                    va='bottom', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                            ax.text(pos, left + valor[pos] / 2 - 0.8 * bar_height, f"{porcentaje_valor:.1f}%",
                                    va='top', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size']-2,
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                        else:
                            texto = f"{valor[pos]:,.0f}" if valor_barra else ""
                            if porcentaje_barra:
                                porcentaje_valor = (valor[pos] / total_valor) * 100 if total_valor > 0 else 0
                                texto += f" ({porcentaje_valor:.1f}%)" if valor_barra else f"{porcentaje_valor:.1f}%"
                            ax.text(pos, left + valor[pos] / 2, texto, va='center', ha='center',
                                    fontsize=font_config['valor_porcentaje_barra']['size'],
                                    fontfamily=font_config['family'],
                                    fontweight=font_config['valor_porcentaje_barra']['weight'],
                                    color=font_config['valor_porcentaje_barra']['color'])
                left += valor[pos]

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            es_espaciador = entidad == "__espaciador__"

        # Determina si es la barra espaciadora
        es_espaciador = False
        if agregar_datos is not None:
            # Detecta si el índice es el espaciador
            if es_fecha:
                # El espaciador es la fecha que no aparece en el resto de los datos (tiene todos ceros)
                if (df_wide.loc[entidad] == 0).all():
                    es_espaciador = True
            else:
                es_espaciador = entidad == "__espaciador__"

        # Mostrar valor total o etiqueta personalizada solo si NO es espaciador
        if valor_total and not es_espaciador:
            # Si hay etiquetas_finales, usa esa etiqueta
            etiqueta_personal = None
            if etiquetas_finales is not None:
                try:
                    etiqueta_personal = etiquetas_finales.iloc[pos]
                except Exception:
                    etiqueta_personal = None
            texto_a_mostrar = f"{int(total_valor):,}"
            if etiqueta_personal is not None and not pd.isna(etiqueta_personal):
                texto_a_mostrar = str(etiqueta_personal)
            if orientacion == 'horizontal':
                espacio = "\u00A0"
                texto_capsula = f"{espacio*4}{texto_a_mostrar}{espacio*4}"
                if not quitar_capsula:
                    t = ax.text(total_valor + x_max * 0.03, pos, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(total_valor + x_max * 0.03, pos, texto_a_mostrar,
                        ha='left', va='center',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
            else:
                espacio = "\u00A0"
                texto_capsula = f"{espacio*2}{texto_a_mostrar}{espacio*2}"
                if not quitar_capsula:
                    t = ax.text(pos, total_valor + x_max * 0.03, texto_capsula,
                        bbox=dict(boxstyle="round,pad=0.15,rounding_size=0.8", facecolor='white', edgecolor='#002F2A', linewidth=1.5),
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])
                    fig.canvas.draw()
                else:
                    ax.text(pos, total_valor + x_max * 0.03, texto_a_mostrar,
                        ha='center', va='bottom',
                        fontsize=font_config['valor_capsula']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['valor_capsula']['weight'],
                        color=font_config['valor_capsula']['color'])

        # Mostrar porcentaje total al inicio de la barra si se solicita
        # ...dentro del ciclo de las barras, en el bloque de porcentaje_total_inicio...
        if porcentaje_total_inicio:
            porcentaje = round((total_valor / total_general) * 100, 1)
            espacio = "\u00A0"
            etiqueta_actual = entidades_formateadas[pos]
            resaltar = resaltar_etiquetas is not None and etiqueta_actual in resaltar_etiquetas
            if resaltar:
                texto_capsula = f"{espacio*1}{etiqueta_actual}   {porcentaje}%{espacio*1}"
                bbox_capsula = dict(facecolor="#a3173e", edgecolor="none", boxstyle="round,pad=0.15,rounding_size=0.8")
                color_texto = "white"
                if orientacion == 'horizontal':
                    ax.text(
                        -x_max*0.02, pos, texto_capsula,
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
                else:
                    ax.text(
                        pos, -x_max*0.02, texto_capsula,
                        ha='center', va='top',
                        rotation=90,  # <-- Esto rota la cápsula y el texto
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=color_texto,
                        bbox=bbox_capsula
                    )
            else:
                # Comportamiento normal: solo porcentaje, sin etiqueta ni cápsula
                if orientacion == 'horizontal':
                    ax.text(
                        0, pos, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )
                else:
                    ax.text(
                        pos, 0, f"{espacio*1}{porcentaje}%{espacio*1}",
                        ha='right', va='top',
                        rotation=90,  # <-- Esto rota el porcentaje solo
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color']
                    )

    # Mostrar porcentaje total al lado derecho de la cápsula o del valor total
        if porcentaje_total:
            porcentaje = round((total_valor / total_general) * 100, 1)
            if orientacion == 'horizontal':
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.18 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        total_valor + desplazamiento, pos, f"{porcentaje}%",
                        ha='left', va='center',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
            else:
                if valor_total and quitar_capsula:
                    desplazamiento = x_max * (0.08 + separar_por_total)
                elif valor_total:
                    desplazamiento = x_max * (0.15 + separar_por_total)
                else:
                    desplazamiento = x_max * (0.03 + separar_por_total)
                ax.text(
                        pos, total_valor + desplazamiento, f"{porcentaje}%",
                        ha='center', va='bottom',
                        fontsize=font_config['porcentaje_total']['size'],
                        fontfamily=font_config['family'],
                        fontweight=font_config['porcentaje_total']['weight'],
                        color=font_config['porcentaje_total']['color'])
                
    # Oculta la etiqueta en el eje si está resaltada y porcentaje_total_inicio está activo
    if porcentaje_total_inicio and resaltar_etiquetas is not None:
        entidades_formateadas = [
            "" if e in resaltar_etiquetas else e
            for e in entidades_formateadas
        ]

    # Configurar ejes y etiquetas
    if orientacion == 'horizontal':
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_yticks(posiciones)
        ax.set_yticklabels(entidades_formateadas, fontsize=font_config['variable_y']['size'],
                           fontweight=font_config['variable_y']['weight'],
                           fontfamily=font_config['family'])
        ax.invert_yaxis()
        ax.set_ylim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_xlim(y_limits)
        else:
            ax.set_xlim(0, 1.1 * x_max)
        ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_xticklabels(),
                 fontsize=font_config['variable_x']['size'],
                 fontweight=font_config['variable_x']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_x']['color'])
        if grillas:
            ax.grid(visible=True, axis='x', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='y', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )
    else:
        x_max = suma_total.max() * 1.15  # <-- AGREGADO
        ax.set_xticks(posiciones)
        ax.set_xticklabels(entidades_formateadas, fontsize=font_config['variable_x']['size'], fontweight=font_config['variable_x']['weight'], fontfamily=font_config['family'], rotation=90, ha='right')
        ax.set_xlim(-0.5, len(entidades)-0.5)
        if y_limits is not None:  # <-- AGREGADO
            ax.set_ylim(y_limits)
        else:
            ax.set_ylim(0, 1.1 * x_max)
        ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins='auto', steps=[1, 2, 5, 10]))
        ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
        plt.setp(ax.get_yticklabels(),
                 fontsize=font_config['variable_y']['size'],
                 fontweight=font_config['variable_y']['weight'],
                 fontfamily=font_config['family'],
                 color=font_config['variable_y']['color'])
        if grillas:
            ax.grid(visible=grillas, axis='y', color='#B9B9B9', linewidth=0.75, linestyle='-')
        else:
            ax.grid(False)
        if porcentaje_total_inicio:
            ax.tick_params(axis='x', pad=espacio_inicio)
        # --- NOMBRE DE EJES ---
        if nombre_eje_x is not None:
            ax.set_xlabel(
                nombre_eje_x,
                fontsize=font_config['nombre_eje_x']['size'],
                fontweight=font_config['nombre_eje_x']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_x']['color'],
                labelpad=18
            )
        if nombre_eje_y is not None:
            ax.set_ylabel(
                nombre_eje_y,
                fontsize=font_config['nombre_eje_y']['size'],
                fontweight=font_config['nombre_eje_y']['weight'],
                fontfamily=font_config['family'],
                color=font_config['nombre_eje_y']['color'],
                labelpad=18
            )    



    # Ajustar rotación de etiquetas especiales según la orientación
    if orientacion == 'vertical':
        for label, entidad in zip(ax.get_xticklabels(), entidades_formateadas):
            if str(entidad) == "Previo":
                label.set_rotation(90)  # Cambia a vertical
                label.set_ha('center')
            elif str(entidad) == "...":
                label.set_rotation(0)
                label.set_ha('center')
    else:  # horizontal
        for label, entidad in zip(ax.get_yticklabels(), entidades_formateadas):
            if str(entidad) == "...":
                label.set_rotation(90)
                label.set_va('center')
            elif str(entidad) == "Previo":
                label.set_rotation(0)
                label.set_va('center')

    if leyenda:
        ncol_leyenda = len(df_wide.columns)
        ax.legend(
            title=leyenda if isinstance(leyenda, str) else None,
            fontsize=font_config['leyenda']['size'],
            title_fontsize=font_config['leyenda']['size'],
            loc='upper center',
            bbox_to_anchor=(0.5, 1.08 + aumenta_sep_leyenda),
            frameon=False,
            ncol=ncol_leyenda
        )

        
    # Quitar bordes
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(False)

    plt.tight_layout()
    plt.savefig(f"{nombre_df}.svg", format='svg', bbox_inches='tight', dpi=300)
    plt.savefig(f"{nombre_df}.png", format='png', bbox_inches='tight', dpi=300)
    nombre_svg = f"{nombre_df}.svg"
    nombre_svg_limpio = f"{nombre_df}_scour.svg"
    nombre_svg_svgo = f"{nombre_df}_svgo.svg"

    try:
        limpiar_svg_con_scour(nombre_svg, nombre_svg_limpio)
    except Exception as e:
        print("Error al limpiar con Scour:", e)

    try:
        limpiar_svg_con_svgo(nombre_svg_limpio, nombre_svg_svgo)
    except Exception as e:
        print("Error al limpiar con SVGO:", e)

    plt.show()

# EXPORTAR: barras_verticales_simples

barras_apiladas(df0, 
                nombre="barras_verticales_simples", 
                bar_height=0.9, 
                font='Montserrat',
                fontsize_barra=20,
                fontsize_valor_total=20,
                ordenar_por='etiqueta',
                orden='ascendente',
                valor_barra=False, 
                porcentaje_barra=False,
                porcentaje_total=True,
                orientacion='vertical',
                area_min=300
                )
Ver carpeta en GitHub