npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@smartopt/charts

v0.5.0

Published

Config-driven chart catalog and dashboard viewer for React. 25 chart types, slicers, light/dark themes. Recharts under the hood.

Readme

@smartopt/charts

Config-driven, yeniden kullanılabilir grafik kataloğu. SmartOpt projelerinde Power BI embed yerine kullanılmak üzere. Recharts üstüne kuruludur.

Mimari sınır: Bu paket sadece UI + grafik mantığıdır. Veri çekme ve authentication host projede kalır — paket veriye hiç dokunmaz. Böylece her müşteri kendi auth sınırı içinde çalışır, cross-auth / embed izin sorunu doğmaz.

Kurulum

npm i @smartopt/charts

react, react-dom ve recharts peer dependency'dir — host projede zaten bulunurlar, paket kendi kopyasını taşımaz. Projende yoksa:

npm i react react-dom recharts

Kaynak: https://bitbucket.org/smartopt/smartoptchartsnpm

Geliştirme ortamı (playground)

Depoyu klonlayıp 25 grafiğin tamamını ve örnek panoyu tarayıcıda görebilirsin:

npm install
npm --prefix playground install
npm run playground     # http://localhost:5173

Playground src/'i doğrudan import eder (build gerekmez); değişiklik anında yansır.

Kullanım

Tek bileşen: ChartRenderer. config ile hangi grafik olduğunu, data ile satırları verirsin. Grafik tipini değiştirmek tek satır config değişikliğidir.

import { ChartRenderer, type ChartConfig } from "@smartopt/charts";

const config: ChartConfig = {
  type: "stacked-bar",          // "bar" | "line" | "area" | "donut" | "kpi" ...
  title: "Aylık Üretim",
  dimensions: ["month"],        // X ekseni
  measures: ["planned", "actual"], // seriler
  options: { stacked: true, format: "compact" },
};

const data = [
  { month: "Oca", planned: 120, actual: 110 },
  { month: "Şub", planned: 140, actual: 135 },
  { month: "Mar", planned: 160, actual: 158 },
];

function Dashboard() {
  // veri projenin kendi API'sinden, kendi auth'u ile gelir
  return <ChartRenderer config={config} data={data} />;
}

Tema — aydınlık / karanlık

İki hazır preset var: lightTheme (= defaultTheme) ve darkTheme. Mod mode prop'u ile seçilir, theme ile üstüne yazılır.

import { Dashboard, ThemeToggle, useThemeMode } from "@smartopt/charts";

function Panel() {
  // Açılışta OS tercihini izler ("system"); kullanıcı seçince ona sabitlenir
  const { mode, setMode } = useThemeMode("system");

  return (
    <>
      <ThemeToggle mode={mode} onChange={setMode} />
      <Dashboard config={config} mode={mode} theme={{ locale: "tr-TR" }} />
    </>
  );
}

Her iki mod ayrı ayrı özelleştirilebilirtheme'e { light, dark } ver:

<Dashboard
  config={config}
  mode={mode}
  theme={{
    light: { accent: "#d97706", palette: ["#0284c7", "#16a34a"], locale: "tr-TR" },
    dark:  { accent: "#e0a417", palette: ["#38bdf8", "#4ade80"], locale: "tr-TR" },
  }}
/>

Düz Partial<ChartTheme> verirsen her iki moda uygulanır; { light, dark } verirsen aktif modun yaması kullanılır. Taban preset her hâlükârda korunur — sadece verdiğin alanlar değişir.

Programatik erişim: resolveTheme(mode, override) nihai temayı döner, useSystemMode() sadece OS tercihini verir.

Tema alanları: palette, grid, axis, text, muted, surface (kart zemini), border, accent (KPI başlık şeridi), accentText, label (data label), locale.

Data label & KPI şeridi

  • options.dataLabels: true → bar/line/area/pie üstüne değerleri yazar.
  • type: "kpi" + title → üstte vurgu renkli (altın) başlık şeridi, altında büyük sayı — Power BI KPI görünümü. options.titleBar: false şeridi kaldırır, showLabel: false alt etiketi gizler. Başlık CSS ile büyük harfe çevrilmez (Türkçe'de "tip" → "TIP" olurdu) — istediğin yazımı config'te ver.
  • Kimlik/kod değerleri için format: "text" — binlik ayracı eklenmez (8000, 8.000 değil).
  • theme.locale: "tr-TR" → tüm eksen/tooltip/label/KPI Türkçe biçimlenir ("Bin" = B, virgül ondalık).

Dashboard (config-driven pano)

Tek bir JSON ile çok grafikli pano. Verinin nereden geleceği config'te tanımlıdır — Dashboard kaynakları çeker, dedupe eder ve her tile'ı grid'e basar. Salt görüntüleme (viewer); son kullanıcı düzenlemez.

import { Dashboard, type DashboardConfig } from "@smartopt/charts";

const config: DashboardConfig = {
  title: "Üretim Panosu",
  layout: { columns: 12, rowHeight: 120, gap: 16 },

  // İsimli kaynaklar: bir API birden çok tile'ı besler → yalnızca 1 kez çekilir
  dataSources: {
    uretim: { url: "/api/uretim", path: "data" }, // yanıt: { data: [ ...satırlar ] }
  },

  tiles: [
    // Aynı "uretim" kaynağı, farklı grafikler (her tile kendi ChartConfig'i)
    {
      id: "kpi-toplam",
      source: "uretim",
      layout: { x: 0, y: 0, w: 3, h: 2 },
      chart: { type: "kpi", measures: ["actual"], dimensions: [],
               options: { aggregate: "sum", format: "compact" } },
    },
    {
      id: "aylik-trend",
      source: "uretim",
      layout: { x: 3, y: 0, w: 9, h: 3 },
      chart: { type: "line", title: "Aylık Trend",
               dimensions: ["month"], measures: ["planned", "actual"] },
    },
    // Ayrı API'si olan tile — kaynağı gömülü tanımla
    {
      id: "hata-dagilim",
      source: { url: "/api/hatalar", method: "POST", body: { range: "2026" } },
      layout: { x: 0, y: 3, w: 6, h: 3 },
      chart: { type: "donut", title: "Hata Dağılımı",
               dimensions: ["type"], measures: ["count"] },
    },
  ],
};

// Auth: host kendi fetcher'ını enjekte eder (token, cookie, cache burada)
const fetcher = async (s) => {
  const res = await fetch(s.url, {
    method: s.method ?? "GET",
    headers: { Authorization: `Bearer ${token}`, ...s.headers },
    body: s.body ? JSON.stringify(s.body) : undefined,
  });
  return res.json();
};

export default function Panel() {
  return <Dashboard config={config} fetcher={fetcher} theme={{ text: "#111827" }} />;
}

Filtreler (slicer)

Power BI'ın filtre bölmesinin karşılığı. Config'e filters eklersin; panel otomatik çizilir ve o sütuna sahip her tile filtrelenir — grafikler anında yeniden hesaplanır.

const config: DashboardConfig = {
  filterPanel: { position: "left", width: 220 },  // "left" | "top"
  filters: [
    // layout VERİLİRSE → grid'de kendi kartında, istediğin yerde
    { id: "tarih", type: "date-range", field: "tarih", label: "Tarih",
      layout: { x: 0, y: 2, w: 4, h: 2 } },
    { id: "urun",  type: "list", field: "urun", label: "Ürün",
      layout: { x: 4, y: 2, w: 3, h: 2 } },

    // layout YOKSA → sol/üst filtre panelinde toplanır
    { id: "adet",  type: "range",  field: "adet",   label: "Adet aralığı" },
    { id: "ara",   type: "search", field: "makine", label: "Makine ara" },
    { id: "vardiya", type: "dropdown", field: "vardiya", default: ["Gündüz"] },
  ],
  tiles: [...],
};

Yerleşim: her filtre bağımsızdır — layout verirsen grafik tile'ları gibi grid'de istediğin konuma oturur (Power BI'da slicer'ın kendi görseli olması gibi); vermezsen panelde toplanır. İkisi karışık kullanılabilir. Panelde hiç filtre kalmazsa panel çizilmez.

| type | kontrol | eşleşme | | --- | --- | --- | | list | onay kutusu listesi (çoklu) | seçilenlerden biri | | dropdown | açılır liste (tek) | seçilen değer | | buttons | yan yana düğmeler (segmented) | seçilenlerden biri | | date-range | iki tarih girişi (+ opsiyonel kaydırıcı) | aralık içinde (bitiş günü dahil) | | range | min / max sayı | aralık içinde | | search | metin kutusu | içerir (harf duyarsız) | | clear | Filtreleri Temizle düğmesi | — (filtre değil) |

Kurallar:

  • Filtre, field sütununa sahip olmayan veri kümelerini etkilemez — farklı API'lerden beslenen tile'lar bir arada güvenle çalışır.
  • appliesTo: ["tileId", ...] ile filtreyi belirli tile'larla sınırlarsın.
  • default ile açılış değeri verilir; Filtreleri Temizle hepsini sıfırlar.
  • Tarih sütunu 2026-07-13, 13.07.2026 ve ISO biçimlerini anlar.

Ek ayarlar:

{ id: "urun", type: "list", field: "urun",
  options: ["A", "B", "C"],   // sabit liste — verilmezse seçenekler veriden türetilir
  selectAll: true },          // başa "Tümünü seç"

{ id: "kirilim", type: "buttons", field: "kirilim",
  options: ["Vardiya", "Günlük"], multiple: false },

{ id: "tarih", type: "date-range", field: "tarih",
  slider: true },             // girişlerin altına çift tutamaklı kaydırıcı

{ id: "temizle", type: "clear", layout: { x: 0, y: 0, w: 2, h: 2 } },

Filtreleme saf fonksiyon olarak da kullanılabilir: applyFilters(rows, filters, state, tileId).

Tek ekrana sığan pano (scroll yok)

Grid'e kesin bir yükseklik ver ve satırları 1fr yap — satırlar boşluğu paylaşır, grafikler de height: "100%" ile kartlarını doldurur:

const config = {
  layout: { columns: 24, rowHeight: "1fr", gap: 6 },   // px yerine fr
  tiles: [
    { id: "t1", source: "s", layout: { x: 0, y: 0, w: 12, h: 8 },
      chart: { type: "bar", dimensions: ["ay"], measures: ["adet"],
               options: { height: "100%" } } },       // sabit px yerine kartı doldur
  ],
};

<Dashboard config={config} style={{ height: "calc(100vh - 80px)" }} />

style grid'e uygulanır; yükseklik kesin olmalı (vh/px/calc), yoksa 1fr satırlar hesaplanamaz.

Grup kartı — tek kartta birden çok grafik

Bir tile'a chart yerine charts verilirse grafikler tek kartın içinde yan yana dizilir. Her alt grafik kendi kaynağını taşıyabilir — pasta + KPI + bar aynı kartta, farklı API'lerden beslenir:

{
  id: "stok-durumu",
  title: "Plastik Fabrikası Stok Durumu",   // grup kartının başlığı
  layout: { x: 0, y: 0, w: 6, h: 8 },
  charts: [
    { flex: 1.4, source: "stok-dagilimi",
      chart: { type: "pie", dimensions: ["kategori"], measures: ["deger"] } },
    { flex: 0.8, source: "maksimum-stok",
      chart: { type: "kpi", title: "Maksimum Stok", dimensions: [],
               measures: ["deger"], options: { titleBar: false } } },
    { width: 90, source: "blokaj",
      chart: { type: "stacked-bar", dimensions: ["kategori"],
               measures: ["blokaj", "bos"],
               options: { xAxis: false, yAxis: false, grid: false } } },
  ],
}

flex genişlik payı, width sabit px'tir. Alt grafiğin source'u yoksa tile'ın source'u kullanılır.

Serbest içerik

content verilen tile grafik yerine host'un verdiği JSX'i çizer — logo, özel düğme, açıklama kartı için kaçış kapısı (JSON değildir):

{ id: "logo", layout: { x: 0, y: 0, w: 2, h: 2 },
  content: <img src={logo} alt="" /> }

Kaynak bağlama kuralları

  • source: "isim"dataSources'taki paylaşımlı kaynak. Aynı isme bağlı tile'lar tek istek paylaşır.
  • source: { url, ... } → tile'a özel API.
  • Aynı veriyi farklı göstermek için her tile'ın kendi chart config'i (farklı dimensions/measures) yeterli — dönüşüm fonksiyonu gerekmez, config saf JSON kalır.
  • path: yanıtın içinde satır dizisine giden nokta-yolu (örn. "data.rows"). Verilmezse yanıtın kökü dizi kabul edilir.
  • Her tile kendi yükleniyor / hata / boş durumunu ayrı gösterir; bir kaynağın hatası diğerlerini düşürmez.

Config şeması

| Alan | Tip | Açıklama | | ------------ | ------------------------------- | ----------------------------------------- | | type | ChartType | Grafik tipi (registry anahtarı) | | title | string? | Başlık | | subtitle | string? | Alt başlık | | dimensions | string[] | Kategori alan(lar)ı; ilki X ekseni | | measures | (string | MeasureDef)[] | Sayısal seriler | | options | ChartOptions? | stacked, smooth, horizontal, format, ... |

MeasureDef: { key, label?, color?, type?, axis?, dataLabels? } — seri başına ad/renk/çizim tipi, değer ekseni ve etiket açık/kapalı vermek için. Sık noktalı bir çizgide dataLabels: false ile o serinin etiketleri kapatılır.

ChartOptions: stacked, smooth, horizontal, legend, legendPosition, dataLabels, grid, xAxis/yAxis (eksen gizleme), height (px ya da "100%"), format (number|compact|percent|currency|text), currency, decimals, aggregate, min/max/target (gauge), bins (histogram), titleBar/showLabel (kpi), labelLine (pie/donut) — artı aşağıdaki görünüm ayarları.

İkincil (sağ) değer ekseni

Ölçekleri çok farklı iki seriyi aynı grafikte göstermek için serilerden biri sağ eksene bağlanır — eksenlere başlık da verilebilir:

{
  type: "combo",
  dimensions: ["donem"],
  measures: [
    { key: "tuketim", label: "Tüketim", type: "bar" },
    { key: "stok", label: "Stok", type: "line", axis: "right" },  // ← sağ eksen
  ],
  options: {
    yAxisLabel: "Tüketim",          // sol eksen başlığı (dikey)
    y2AxisLabel: "Stok",            // sağ eksen başlığı
    y2AxisWidth: 40,
    xAxisLabel: "Dönem",
  },
}

Çok satırlı kategori etiketi

dimensions'a birden çok alan verilirse X ekseni etiketi alt alta yazılır — "sipariş no + makine adı" gibi iki katmanlı etiketler için:

{ type: "bar", dimensions: ["siparis", "makine"], measures: ["uretim"] }
// eksen:  8001296359
//         IMM-10

Renk, eksen yönü ve boyut

Her grafik kendi rengini, eksen yazı açısını ve boyutunu config'ten alabilir:

options: {
  // RENK — bu grafiğe özel palet (temanın paletini ezer)
  palette: ["#0ea5e9", "#f43f5e", "#22c55e"],

  // EKSEN YAZI YÖNÜ — derece; negatif = sola eğik
  xTickAngle: -45,     // X etiketlerini eğ (uzun kategori adları için)
  yTickAngle: 0,
  xAxisHeight: 80,     // eğik yazılar kesiliyorsa yer aç
  yAxisWidth: 90,      // uzun Y etiketleri için genişlik

  // BOYUT
  height: 300,         // px (varsayılan 320)
  width: 600,          // px — verilmezse kapsayıcıyı doldurur (%100)
  margin: { top: 16, right: 24, bottom: 8, left: 8 },
  barSize: 32,         // bar kalınlığı üst sınırı
  innerRadius: "60%",  // donut/pie/gauge/radial yarıçapları
  outerRadius: "85%",
}

Renk önceliği: measure.coloroptions.palettetheme.palette. Yani tek bir seriyi measures: [{ key, color }] ile, tüm grafiği options.palette ile, tüm panoyu theme.palette ile boyarsın.

Yazı boyutu / rengi

Her metin katmanı ayrı ayarlanır — hepsi { size?, color?, weight? }. Hepsi mod-duyarlıdır: düz değer her iki modda geçerlidir, { light, dark } verirsen mod başına ayrışır (palette dahil):

options: {
  palette:    { light: ["#0284c7", "#16a34a"], dark: ["#38bdf8", "#4ade80"] },
  titleStyle: { light: { color: "#111827" },   dark: { color: "#f9fafb" } },
  labelStyle: { light: { color: "#374151", size: 12 },
                dark:  { color: "#e5e7eb", size: 12 } },
}

Düz kullanım (her iki modda aynı):

options: {
  labelStyle:  { size: 13, color: "#fff", weight: 600 },  // değer etiketleri
  axisStyle:   { size: 11, color: "#9ca3af" },            // eksen yazıları
  legendStyle: { size: 12, color: "#e5e7eb" },            // legend
  valueStyle:  { size: 40, color: "#22c55e", weight: 800 },// kpi/gauge büyük sayı
  titleStyle:  { size: 16, color: "#fff", weight: 700 },  // başlık
  cellStyle:   { size: 12, color: "#e5e7eb" },            // table/matrix hücre
}

Responsive (kırılımlar)

layout.breakpoints verilirse pano kendi genişliğini ölçer (viewport'u değil — bir sidebar'ın yanında da doğru davranır) ve uyan en dar kırılımı uygular. Verilmezse yerleşim hiç değişmez, davranış eskisi gibidir.

layout: {
  columns: 24, rowHeight: 32, gap: 6,
  breakpoints: [
    { name: "md", maxWidth: 1024, columns: 12, rowHeight: 46 },
    { name: "sm", maxWidth: 640,  columns: 4,  rowHeight: 42 },
  ],
}

Her kart kırılımda önce layouts[ad]ine bakar; yoksa layout genişliği oransal ölçeklenir ve x/y düşürülür (kart akışa girer):

{ id: "satis", layout: { x: 3, y: 3, w: 6, h: 10 },
  layouts: {
    md: { x: 0, y: 14, w: 6, h: 9 },
    sm: { x: 0, y: 34, w: 4, h: 10 },
  } }

layouts.sm = { hidden: true } ile kart o kırılımda hiç çizilmez. Slicer'lar (FilterDef) da aynı layout / layouts alanlarını kullanır.

CSS'in ulaşamadığı grafik ayarları (çubuk kalınlığı, eksen, legend) kırılım başına ezilebilir:

options: {
  barSize: 54,
  responsive: {
    sm: { barSize: 14, dataLabels: false, axisStyle: { size: 8 } },
  },
}

Pano kökü soc-dash ve soc-dash--<kırılım adı> (kırılım yoksa soc-dash--base) sınıflarını taşır — host CSS buradan tutunur.

İpucu: satır yüksekliği kesin bir px olduğunda options.height: "100%" çalışır; grafik kartın kalan alanını doldurur ve her kırılımda kendiliğinden doğru boyda çıkar. Yükseklikleri elle hesaplamana gerek kalmaz.

Sınıf kancaları (projeye özel ince ayar)

Kütüphane sabit sınıf kancaları verir. Görünüm ayarı önce prop ile denenir (her projede aynı şekilde çalışsın diye); yalnızca o projeye özel bir tweak gerekiyorsa CSS'e inilir.

| sınıf | öğe | | --- | --- | | soc-tile, soc-tile--kpi/--group/--content | pano kartı | | soc-tile-title, soc-tile-group, soc-tile-group-item | grup kartı parçaları | | soc-chart, soc-chart--<type> | grafik kökü | | soc-chart-title, soc-chart-subtitle | grafik başlığı | | soc-kpi, soc-kpi-title, soc-kpi-value, soc-kpi-label, soc-kpi-body | KPI parçaları | | soc-filter-tile, soc-filter-tile--<type> | slicer kartı | | soc-filter-field, soc-filter-label | slicer etiketi/sarmalayıcısı | | soc-filter-input, soc-filter-button, soc-filter-clear, soc-filter-list | slicer kontrolleri |

Ek sınıf vermek için: TileDef.className, ChartOptions.className, FilterDef.className.

Kutu ölçüleri inline DEĞİL. Dolgu, taşma, taban yükseklik ve giriş kutusu yazı boyu tek seferlik enjekte edilen bir stil bloğundan gelir (src/baseStyles.ts). Varsayılanlar aynıdır, ama sınıf seçici oldukları için host !important yazmadan ezebilir — dar bir panoda kartları sıkılaştırmanın yolu budur. Renk/tipografi temadan, yerleşim (flex/grid) bileşenden gelir; onlar inline kalır.

/* host projede — yalnız bu panoya özel */
.pano .soc-tile--kpi .soc-kpi-value { letter-spacing: -0.5px; }

/* dar panoda slicer kartlarını sıkılaştır */
.pano .soc-filter-tile { padding: 8px; overflow: hidden; }
.pano .soc-filter-input { padding: 4px 7px; font-size: 12px; }

Hazır grafikler

| type | açıklama | dimensions | measures | | --- | --- | --- | --- | | bar / line / area | çubuk / çizgi / alan | X ekseni | seriler | | stacked-bar / stacked-area | yığılı çubuk / alan | X ekseni | seriler | | stacked-bar-100 / stacked-area-100 | %100 yığılı (normalize) | X ekseni | seriler | | combo | bar + çizgi karışık (measure başına type) | X ekseni | seriler | | combo-stacked | yığılı sütun + çizgi | X ekseni | seriler | | ribbon | şerit grafiği (sıralama değişimi) | X ekseni | seriler | | donut / pie | halka / pasta | dilim adı | tek değer | | radar | örümcek ağı | açı ekseni | seriler | | radial | eş-merkezli halka çubuk | halka adı | tek değer | | gauge | tek değerli yay göstergesi (min/max/target) | — | tek değer | | scatter | dağılım | grup (ops.) | X, Y, boyut | | funnel | huni | aşama | tek değer | | waterfall | şelale (kümülatif artış/azalış) | kalem | delta | | histogram | dağılım/frekans (bins) | — | tek sütun | | treemap | oranlı kutular | ad | tek değer | | table | tablo | sütunlar | sütunlar | | matrix | pivot tablo + toplamlar | satır, sütun | tek değer | | kpi | tek büyük sayı + başlık şeridi | — | tek değer | | kpi-trend | değer + hedef + sapma + trend sparkline | trend ekseni | değer, hedef | | multi-row-card | çok satırlı metrik kartı | — | ölçüler |

Power BI kapsam notu: harita (geo/tile servisi), yapay zeka görselleri (Key influencers, Decomposition tree, Q&A, Smart narrative), R/Python ve ribbon görselleri kapsam dışı — dış servis/özel motor gerektirirler.

Yeni grafik ekleme

Registry genişletilebilir — kütüphaneyi değiştirmeden yeni tip eklenebilir:

import { registerChart, useChartTheme, type ChartComponentProps } from "@smartopt/charts";

function Gauge({ config, data }: ChartComponentProps) {
  const theme = useChartTheme();
  // ... kendi grafiğin
  return <div />;
}

registerChart("gauge", Gauge);
// artık <ChartRenderer config={{ type: "gauge", ... }} /> çalışır

Geliştirme

npm install
npm run build      # dist/ üretir (esm + cjs + d.ts)
npm run dev        # watch modu
npm run typecheck

Yol haritası

  • [x] Dashboard viewer (config-driven pano + API bağlama)
  • [ ] responsive breakpoint'ler (mobilde tek sütun)
  • [ ] cross-filtering (bir grafiğe tıkla → diğerlerini filtrele)
  • [ ] drill-down
  • [ ] slicer / global filtre bileşeni
  • [ ] combo (bar + line) grafiği
  • [ ] table / matrix, heatmap, gauge
  • [ ] config → görsel builder (son kullanıcı düzenlesin)
  • [ ] JSON Schema ile config doğrulama