Buttons Do Not Resize Proportionally in Custom Panel

Title: Buttons Do Not Resize Proportionally in Custom Panel

Hi everyone,

I’ve created a custom panel that displays services as clickable boxes (with buttons and animations) using ECharts in Grafana. However, when I resize the panel (make it smaller or larger), the buttons do not scale proportionally with the panel. Their size stays fixed, which breaks the layout.

I’m looking for a way to make the buttons and text responsive to the panel size (or any workaround).
Can someone guide me or point me in the right direction? :folded_hands:

:puzzle_piece: ECharts Code (Volkovlabs Plugin)

function wrapText(text, maxLen = 16) {
  const cleanedText = text?.toString().replace(/Servisi/gi, '').trim() || '';
  const words = cleanedText.split(' ');
  let lines = [], current = '';

  for (const word of words) {
    if ((current + word).length <= maxLen) {
      current += word + ' ';
    } else {
      lines.push(current.trim());
      current = word + ' ';
    }
  }
  if (current) lines.push(current.trim());
  return lines.join('\n');
}

function getOption() {
  const data = context.panel.data.series?.[0];
  if (!data) {
    return { title: { text: '❌ Veri yok', left: 'center', top: 'middle' } };
  }

  const serviceField = data.fields.find(f => f.name.toLowerCase().includes('servis'));
  const severityField = data.fields.find(f => f.name.toLowerCase().includes('severity') || f.name.toLowerCase().includes('value'));
  const tooltipField = data.fields.find(f => f.name.toLowerCase().includes('tooltip'));

  if (!serviceField || !severityField || !tooltipField) {
    return { title: { text: '❌ Alanlar eksik', left: 'center', top: 'middle' } };
  }

  const services = serviceField.values.toArray();
  const severities = severityField.values.toArray();
  const tooltips = tooltipField.values.toArray();

  const boxWidth = 140;
  const boxHeight = 70;
  const boxSpacing = 3;
  const itemsPerRow = 13;
  const minFontSize = 10;
  const maxFontSize = 14;

  const servisLinkleri = {
    "Ankes": "https://kokpitizl.vakifbank.intra/d/ee6h5un4vi9kwd/ankes-servisi?orgId=1",
    "Banka Kartları Yönetimi": "https://kokpitizl.vakifbank.intra/d/ce6iams4c2ayod/banka-kartlari-yonetimi-servisi?orgId=1",
    "Banka Sigortacılığı": "https://kokpitizl.vakifbank.intra/d/ce6kzvbpcnvnke/banka-sigortaciligi-servisi?orgId=1",
    "Bireysel Krediler": "https://kokpitizl.vakifbank.intra/d/ce6l7r2py81kwe/breysel-krediler-servisi?orgId=1",
    "Çek-Senet": "https://kokpitizl.vakifbank.intra/d/fe6lg38zr1w5ca/cek-senet-servisi?orgId=1",
    "Dış İşlemler": "https://kokpitizl.vakifbank.intra/d/ee6lk9oqm9hq8c/dis-islemler-servisi?orgId=1",
    "Fiyatlama Yönetimi": "https://kokpitizl.vakifbank.intra/d/ce6lnwxh9of7ka/fiyatlama-yonetimi-servisi?orgId=1",
    "Hazine İşlemleri": "https://kokpitizl.vakifbank.intra/d/ae6lr66brz94wb/hazine-islemleri-servisi?orgId=1",
    "Kredi Kartları Yönetimi": "https://kokpitizl.vakifbank.intra/d/ee6mfr3j1bls0f/kredi-kartlari-yonetimi-servisi?orgId=1",
    "Kredili Mevduat": "https://kokpitizl.vakifbank.intra/d/fe6mi7w5pfwn4f/kredili-mevduat-servisi?orgId=1",
    "Kur ve Döviz Yönetimi": "https://kokpitizl.vakifbank.intra/d/fe6mk2elgh0cgb/kur-ve-doviz-yonetimi-servisi?orgId=1",
    "Kurum Ödemeleri": "https://kokpitizl.vakifbank.intra/d/ce6mnb5a6uk8we/kurum-odemeleri-servisi?orgId=1",
    "Kurum Tahsilatları": "https://kokpitizl.vakifbank.intra/d/fe6mp83hwxfcwe/kurum-tahsilatlari-servisi?orgId=1",
    "Mevduat": "https://kokpitizl.vakifbank.intra/d/fe6mp83hwxfcwe/kurum-tahsilatlari-servisi?orgId=1",
    "Para Transferleri": "https://kokpitizl.vakifbank.intra/d/fe6mp83hwxfcwe/kurum-tahsilatlari-servisi?orgId=1",
    "Ticari Krediler": "https://kokpitizl.vakifbank.intra/d/fe6nzym5dm48wd/ticari-krediler-servisi?orgId=1",
    "Yatırım İşlemleri": "https://kokpitizl.vakifbank.intra/d/fe6o8v6n9zshse/yatirim-islemleri-servisi?orgId=1"
  };

  const graphic = [];

  services.forEach((name, idx) => {
    const severity = parseInt(severities[idx] ?? 0);
    const tooltipText = tooltips[idx] || name;

    let color = '#D9D9D9';
    let needsAnimation = false;

    if (severity === 1) {
      color = '#ffc000';
      needsAnimation = true;
    } else if (severity === 2) {
      color = '#ff0000';
      needsAnimation = true;
    }

    const cleanedName = name.replace(/Servisi/gi, '').trim();
    const row = Math.floor(idx / itemsPerRow);
    const col = idx % itemsPerRow;
    const x = col * (boxWidth + boxSpacing);
    const y = row * (boxHeight + boxSpacing);

    const wrapped = wrapText(name);
    const lines = wrapped.split('\n');
    const calculatedFontSize = Math.min(maxFontSize, Math.max(minFontSize, Math.floor(boxWidth / 8)));
    const lineHeight = calculatedFontSize + 2;
    const totalTextHeight = lines.length * lineHeight;
    const textY = y + (boxHeight - totalTextHeight) / 2 + lineHeight / 2;

    const rectObj = {
      type: 'rect',
      name: cleanedName,
      shape: { x, y, width: boxWidth, height: boxHeight },
      style: {
        fill: color,
        stroke: '#000',
        lineWidth: 1,
        cursor: servisLinkleri[cleanedName] ? 'pointer' : 'default',
        shadowBlur: 8,
        shadowColor: color
      },
      tooltip: {
        show: true,
        formatter: () => {
          if (severity === 1) {
            return `<div style="font-size:15px; font-weight:bold; color:#ff9900;">⚠️ ${tooltipText}</div>`;
          } else if (severity === 2) {
            return `<div style="font-size:16px; font-weight:bold; color:#ff3333; text-shadow: 1px 1px #000;">🔥 ${tooltipText}</div>`;
          } else {
            return tooltipText;
          }
        }
      },
      onclick: servisLinkleri[cleanedName] ? () => window.open(servisLinkleri[cleanedName], '_blank') : undefined
    };

    if (needsAnimation) {
      rectObj.keyframeAnimation = {
        duration: 1000,
        loop: true,
        keyframes: [
          { percent: 0, style: { opacity: 1 } },
          { percent: 0.5, style: { opacity: 0.2 } },
          { percent: 1, style: { opacity: 1 } }
        ]
      };
    }

    graphic.push(rectObj);

    lines.forEach((line, i) => {
      graphic.push({
        type: 'text',
        style: {
          text: line,
          x: x + boxWidth / 2,
          y: textY + i * lineHeight,
          textAlign: 'center',
          textVerticalAlign: 'middle',
          fill: '#000',
          font: `bold ${calculatedFontSize}px Segoe UI, sans-serif`,
          lineHeight: lineHeight
        }
      });
    });
  });

  return {
    backgroundColor: '#111',
    tooltip: {
      show: true,
      confine: true,
      backgroundColor: '#000',
      borderColor: '#666',
      borderWidth: 1,
      padding: 10,
      textStyle: {
        fontFamily: 'Segoe UI',
        fontSize: 13,
        color: '#00ffcc'
      }
    },
    graphic
  };
}

return getOption();