@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.
Maintainers
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/chartsreact, react-dom ve recharts peer dependency'dir — host projede zaten
bulunurlar, paket kendi kopyasını taşımaz. Projende yoksa:
npm i react react-dom rechartsKaynak: 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:5173Playground 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ştirilebilir — theme'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: falsealt 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.000değ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,
fieldsü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.defaultile açılış değeri verilir; Filtreleri Temizle hepsini sıfırlar.- Tarih sütunu
2026-07-13,13.07.2026ve 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
chartconfig'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-10Renk, 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.color → options.palette → theme.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ışırGeliştirme
npm install
npm run build # dist/ üretir (esm + cjs + d.ts)
npm run dev # watch modu
npm run typecheckYol 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
