İçeriğe geç
Teknik YazıVisualization

One Piece Hub: Büyük Bir Evreni Görselleştirmek

66 karakter ve yüzlerce ilişkiyi sıkıcı bir listeye boğmak yerine, d3-force ile canlı ve anlaşılır bir karakter grafı kurarken aldığım temel kararlar.

15 Nisan 202610 dk okuma

One Piece Hub için karakter sayfasını hazırlarken önümde net bir problem vardı: karakter çoktu, ilişkiler daha da fazlaydı. Kaptanlar, tayfa arkadaşları, rakipler, aile bağları. Bunları düz bir tabloda göstermek mümkün ama tadı yoktu. Bilgi vardı ama evren hissi yoktu.

Bu yüzden çözümü ilişki grafında aradım. Force-directed yaklaşımı burada çok doğal duruyor. Birbirine bağlı karakterler yakın duruyor, ortak bağı olanlar merkeze geliyor, alakasız gruplar kendiliğinden ayrılıyor. Yani yerleşimi tek tek elle yapmak yerine fiziğe bırakıyorsun.

Veriyi Nasıl Düzenledim?

İlk düzgün karar veriyi temiz ayırmak oldu. Karakterler ayrı, ilişkiler ayrı tutulunca hem kod rahatlıyor hem de aynı veri ileride başka yerde de kullanılabiliyor. Ayrıca ilişkinin türünü ve gücünü ayrı ayrı yazmak, grafın davranışını daha anlamlı hale getiriyor.

ts
export interface CharacterNode {
  id: string
  name: string
  crew: string | null
  bounty: number | null
  avatar: string
}

export type RelationKind =
  | 'crewmate'
  | 'captain'
  | 'rival'
  | 'family'
  | 'mentor'
  | 'enemy'

export interface Relation {
  source: string
  target: string
  kind: RelationKind
  weight: number
}

İpucu

Her ilişki eşit değil. Ağırlık değeri eklemek, bazı bağların gerçekten daha kuvvetli görünmesini sağlıyor.

Önce Kısa Bir Fizik Mantığı

d3-force kullanmadan önce ufak bir fizik denemesi yapmak çok faydalı oldu. Düğümler birbirini itiyor, bağlı olanlar birbirini çekiyor, sistem de hafifçe merkeze dönmeye çalışıyor. İçeride ne döndüğünü görünce hazır kütüphaneyi ayarlamak da kolaylaşıyor.

ts
interface Point { x: number; y: number; vx: number; vy: number }

function step(nodes: Point[], links: [number, number][], dt: number) {
  for (let i = 0; i < nodes.length; i++) {
    for (let j = i + 1; j < nodes.length; j++) {
      const a = nodes[i], b = nodes[j]
      const dx = b.x - a.x, dy = b.y - a.y
      const dist = Math.hypot(dx, dy) || 0.01
      const force = 800 / (dist * dist)
      const fx = (dx / dist) * force
      const fy = (dy / dist) * force
      a.vx -= fx; a.vy -= fy
      b.vx += fx; b.vy += fy
    }
  }
}

Neden d3-force Kullandım?

Kendi yazdığın fizik küçük örnekte öğretici ama veri büyüdükçe yorucu hale geliyor. d3-force burada hem performans hem de ayar kolaylığı açısından çok daha iyi çalıştı. Çarpışma, merkezleme ve bağlantı kuvvetlerini tek tek ayarlayabiliyorsun.

ts
return forceSimulation(nodes)
  .force('link',
    forceLink(links)
      .id((d) => d.id)
      .distance((d) => 120 - d.weight * 80)
      .strength((d) => d.weight),
  )
  .force('charge', forceManyBody().strength(-380))
  .force('center', forceCenter(0, 0))
  .force('collide', forceCollide(38))

Bilgi

Simülasyonun biraz uzun sürmesi her zaman kötü değil. Yerleşim temiz oturuyorsa kullanıcı bunu doğrudan kalite olarak hissediyor.

React ile Beraber Kullanım

Buradaki önemli nokta şu: her tick anında React state güncellemek istemiyorsun. Bu pahalı. Daha rahat çalışan yol, React’in kabuğu kurması ve sonra SVG öğelerini d3’ün doğrudan güncellemesi oldu.

tsx
useEffect(() => {
  const sim = createSimulation(nodes, links)

  sim.on('tick', () => {
    for (const n of sim.nodes()) {
      const el = nodeEls.get(n.id)
      if (el) el.setAttribute('transform', `translate(${n.x},${n.y})`)
    }
  })

  return () => sim.stop()
}, [nodes, links])

Mobilde Aynı Şeyi Zorlamadım

Masaüstünde çok iyi duran ilişki grafı, mobilde aynı rahatlığı vermiyor. Düğümler sıkışıyor, sürükleme ile sayfa kaydırma birbirine giriyor. O yüzden mobilde grafı kapatıp daha okunur bir avatar düzenine geçmek daha mantıklı oldu.

İpucu

Her özelliği her ekrana aynı haliyle taşımak zorunda değilsiniz. Aynı bilginin farklı cihazlarda farklı sunulması çoğu zaman daha iyi sonuç verir.