عقدة HTML مخصصة
عندما لا تكون هناك مجموعة من shape وcolor وicon وtext كافية، قم بتسليم العارض بعضًا العلامات. html هي قناة ذات نمط عادي، لذا يتم حل البطاقة من خلال نفسه سلسلة الأسبقية مثل أي سلسلة أخرى - مما يعني أن البطاقة هي شيء تعطيه لـ نوع واحد من العقدة، وليس الرسم البياني بأكمله.
كل عقدة أدناه تخرج من nodeStyleMap:
| عقدة | أعلن كما | ما تحصل عليه |
|---|---|---|
| بطاقات الفريق الثلاثة | shape: 'none' + html | البطاقة is العقدة - لا يوجد شكل خلفها، ومربعها المقاس هو الذي يحرك التخطيط والحواف |
| عند الاتصال | shape: 'circle' + html | لا يزال القرص مرسومًا، ولا يزال size هو الأرضية - حبة تركب على شكل |
staging · prod | لا html | أشكال سداسية بسيطة، من نفس الخريطة |
| Q3 تجميد | shape: 'none'، لا html | تسمية عارية: لا يوجد شيء مرسوم سوى النص، ولا يزال قابلاً للسحب والاختيار |
| العمليات | العقدة الخاصة style.html | عقدة واحدة تتجاوز بطاقة نوعها |
| هندسة | render.renderNode | رد اتصال عالمي بطاقة فقط هذه العقدة ولا يُرجع شيئًا للباقي |
أربعة أشياء تستحق الاستفزاز:
- انقر فوق البطاقة. يتم تمييزها مثل أي عقدة أخرى. عقدة البطاقة فقط تبقي غير مرئية المربع الموجود أسفله على وجه التحديد بحيث يكون للمظهر المحدد والمتحرك شيء ما يهبط عليه - بدونها، لن يكون للعقدة المخصصة أي حالة محددة على الإطلاق.
- اسحب البطاقات حولها. قوة الاصطدام تباعد بين صناديق البطاقات حقيقية، و تتوقف الحواف عند حدود كل بطاقة وليس على دائرة مرسومة حولها. لا شيء عنه كان لا بد من ضبط التخطيط لذلك.
- قارن
OperationsبـOn call. كلاهماhtml. واحد فقط لديه شكل خلفه، وهذا هو كل ما يفعلهshape: 'none'. - انظر إلى علامة
Operations'. جذرها هوwidth: 100%— الشكل الذي كان يستخدم طيها على مربع العنصر النائب ثم قصها. يتم قياس البطاقات داخل علبة قابلة للانكماش مربع، لذلك يتم حله وفقًا لمحتواه الخاص وليس هناك ما عليك القيام به.
العلامات الموثوقة فقط
يتم إدراج كل ما تعيده كما هو، لذلك تم تصميم هذه البطاقات بـ textContent بدلاً من ذلك بدلاً من استيفاء بيانات العقدة في سلسلة ترميزية. انظر الأمان.
js
/** A team card. Built with `textContent`, never by interpolating data into markup. */
function teamCard(node) {
const d = node.getData()
const card = document.createElement('div')
card.setAttribute('style', [
'display: flex',
'align-items: center',
'gap: 8px',
'padding: 8px 12px',
'border-radius: 12px',
'background: #ffffff',
`border: 2px solid ${d.tone}`,
'box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18)',
'font-family: system-ui, sans-serif',
'white-space: nowrap',
].join(';'))
const emoji = document.createElement('span')
emoji.style.fontSize = '20px'
emoji.textContent = d.emoji
const stack = document.createElement('span')
stack.setAttribute('style', 'display: flex; flex-direction: column; line-height: 1.2')
const title = document.createElement('strong')
title.setAttribute('style', 'font-size: 13px; color: #0f172a')
title.textContent = d.name
const lead = document.createElement('span')
lead.setAttribute('style', `font-size: 11px; color: ${d.tone}`)
lead.textContent = d.lead
stack.append(title, lead)
card.append(emoji, stack)
return card
}
/** A small pill, drawn on top of the shape it is given rather than replacing it. */
function rotaPill(node) {
const pill = document.createElement('div')
pill.setAttribute('style', [
'display: inline-flex',
'padding: 2px 8px',
'border-radius: 999px',
'background: #0f172a',
'color: #fff',
'font: 600 10px system-ui, sans-serif',
'white-space: nowrap',
].join(';'))
pill.textContent = node.getData().lead
return pill
}
const options = {
render: {
nodeTypeAccessor: (node) => node.getData()?.kind,
nodeStyleMap: {
// A card and nothing else. `shape: 'none'` is what makes it the whole node:
// no disc behind it, and the node's radius and edge anchors come from the
// measured card instead of from `size`.
team: {
shape: 'none',
html: teamCard,
// The card carries its own title, so switch the SVG label off. `undefined`
// would fall through to whatever the chain said; `''` is how you clear it.
text: '',
},
// The other way round: the shape is still drawn, and `size` is still the
// smallest the node may be, so the pill rides on a coloured disc.
rota: {
shape: 'circle',
size: 30,
color: '#f59e0b',
html: rotaPill,
text: (node) => node.getData()?.name,
textVerticalShift: -1.5,
},
// No card at all — ordinary shapes, from the same style map.
env: {
shape: 'hexagon',
size: 22,
color: '#16a34a',
text: (node) => node.getData()?.name,
textVerticalShift: -1.4,
},
// Shapeless with no card either: a label floating on the canvas. Still
// draggable and selectable, over `2 × size`.
milestone: {
shape: 'none',
size: 26,
text: (node) => node.getData()?.name,
// Nothing behind it to shorten against, so draw it whole.
textTruncate: false,
},
},
// A global callback can card *some* nodes now: returning nothing hands the node
// back to the chain above, so everything else is styled as usual. Here it draws
// the badge on whichever team owns the release.
renderNode: (node) => {
if (node.id !== 'eng') return undefined
const card = teamCard(node)
card.style.borderWidth = '3px'
const flag = document.createElement('span')
flag.setAttribute('style', [
'margin-left: 8px',
'padding: 2px 6px',
'border-radius: 6px',
'background: #fee2e2',
'color: #b91c1c',
'font: 700 10px system-ui, sans-serif',
].join(';'))
flag.textContent = 'RELEASE OWNER'
card.append(flag)
return card
},
defaultEdgeStyle: { markerEnd: 'arrow', strokeWidth: 1.5 },
},
simulation: {
d3LinkDistance: 110,
d3ManyBodyStrength: -700,
d3CollideRadiusMultiplier: 1.15,
},
UI: {
mode: 'full',
minimap: false,
table: false,
// This card is about the nodes, so nothing else competes for the canvas.
legend: false,
},
}js
const data = {
nodes: [
// Cards, from a `nodeStyleMap` entry.
{ id: 'design', data: { kind: 'team', name: 'Design', lead: 'Lena', emoji: '🎨', tone: '#8b5cf6' } },
{ id: 'eng', data: { kind: 'team', name: 'Engineering', lead: 'Raj', emoji: '⚙️', tone: '#2563eb' } },
{ id: 'data', data: { kind: 'team', name: 'Data', lead: 'Omar', emoji: '📊', tone: '#0891b2' } },
// A card on a shape: the disc is still drawn behind it.
{ id: 'oncall', data: { kind: 'rota', name: 'On call', lead: 'Maya' } },
// Plain shapes, from the same style map.
{ id: 'staging', data: { kind: 'env', name: 'staging' } },
{ id: 'prod', data: { kind: 'env', name: 'prod' } },
// A label and no shape at all.
{ id: 'q3', data: { kind: 'milestone', name: 'Q3 freeze' } },
// One node overriding its type's card with its own.
{
id: 'ops', data: { kind: 'team', name: 'Operations', lead: 'Theo', emoji: '🧭', tone: '#ea580c' },
style: {
shape: 'none',
html: (node) => {
const d = node.getData()
const card = document.createElement('div')
// `width: 100%` and it still measures its content: the card is
// measured inside a shrink-to-fit box, not the placeholder.
card.setAttribute('style', [
'display: flex',
'align-items: center',
'gap: 8px',
'width: 100%',
'padding: 10px 14px',
'border-radius: 999px',
`background: ${d.tone}`,
'color: #fff',
'font: 600 13px system-ui, sans-serif',
'white-space: nowrap',
].join(';'))
card.textContent = `${d.emoji} ${d.name} — always on`
return card
},
},
},
],
edges: [
{ from: 'design', to: 'eng' },
{ from: 'eng', to: 'data' },
{ from: 'ops', to: 'eng' },
{ from: 'oncall', to: 'ops' },
{ from: 'eng', to: 'staging' },
{ from: 'staging', to: 'prod' },
{ from: 'q3', to: 'prod' },
{ from: 'data', to: 'prod' },
{ from: 'design', to: 'ops' },
],
}راجع العرض → HTML العقد للاطلاع على العقد الكامل - عندما تكون البطاقة تم قياس ما تفعله البطاقة وليس إيقاف التشغيل، وتمشيط بعض العقد فقط.