Skip to content

Instantly share code, notes, and snippets.

@suminb
Created May 11, 2020 13:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save suminb/63f8a45436864e7407129e713e5641ba to your computer and use it in GitHub Desktop.
Save suminb/63f8a45436864e7407129e713e5641ba to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
.hover path {
stroke: #ccc;
}
.hover text {
fill: #ccc;
}
.hover g.primary text {
fill: black;
font-weight: bold;
}
.hover g.secondary text {
fill: #333;
}
.hover path.primary {
stroke: #333;
stroke-opacity: 1;
}
</style>
</head>
<body>
<div id="graph"></div>
<script>
const svg = d3.select("#graph")
.append("svg")
.attr("width", 800)
.attr("height", 2000);
function arc(d) {
const y1 = d.source.y;
const y2 = d.target.y;
const r = Math.abs(y2 - y1) / 2;
return `M${margin.left},${y1}A${r},${r} 0,0,${y1 < y2 ? 1 : 0} ${margin.left},${y2}`;
}
const margin = ({ top: 20, right: 20, bottom: 20, left: 120 });
const step = 14;
const data = {
// 모든 등장 인물을 옮겨 적지는 못하고 다음의 조건을 만족하는 인물들만 정리했다.
// 1. 스토리가 있는 인물
// 2. 별다른 스토리는 없지만 관계 정의상 필요한 인물
nodes: [
{ id: "송수정", group: 1 },
{ id: "송수정 엄마", group: 1 },
{ id: "송수정 남자친구", group: 1 },
{ id: "이기윤", group: 2, description: "응급학과 레지던트 1년차" },
{ id: "인턴 1", group: 2, },
{ id: "승희", group: 2, description: "남자친구에게 살해된 피해자" },
{ id: "권혜정", group: 3, description: "간호사" },
{ id: "정형외과 교수", group: 3, description: "50대 남성" },
{ id: "인턴 2", group: 3, description: "권혜정과 팔씨름" },
{ id: "조양선", group: 4, description: "희 엄마" },
{ id: "조양선 오빠", group: 4, description: "" },
{ id: "성식", group: 4, description: "승희 아빠" },
{ id: "김성진", group: 5, description: "병원 보안요원, 동성애자" },
{ id: "강한정", group: 5, description: "반사회성 인격장애" },
{ id: "최애선", group: 6, description: "" },
{ id: "최애선 첫째 며느리", group: 6, description: "" },
// { id: "최애선 둘째 아들", group: 6, description: "" },
{ id: "윤나", group: 6, description: "최애선 둘째 며느리" },
{ id: "임대열", group: 7, description: "이비인후과 의사, 고막 브레이커, 기러기 아빠" },
{ id: "인턴 3", group: 7, description: "임대열이 때린 인턴, 울보, 소씨" },
{ id: "임대열 부인", group: 7, description: "" },
{ id: "임대열 자식 1", group: 7, description: "" },
{ id: "임대열 자식 2", group: 7, description: "" },
{ id: "레지던트 1", group: 7, description: "임대열과 갈등" },
{ id: "장유라", group: 8, description: "" },
{ id: "오헌영", group: 8, description: "교통사고" },
{ id: "오정빈", group: 8, description: "장유라, 헌영 딸" },
{ id: "화물연대 사람들", group: 8, description: "" },
{ id: "이환의", group: 9, description: "윤나의 남편, CT실 근무" },
{ id: "유채원", group: 10, description: "수술실 주니어 스태프" },
{ id: "환자 1", group: 10, description: "84세 할머니, 병원장 뒷바라지, 부동산 큰손" },
{ id: "내과 교수 1", group: 10, description: "환자 1의 내시경 담당" },
{ id: "브리타 훈겐", group: 11, description: "" },
{ id: "문우남", group: 11, description: "" },
{ id: "진선미", group: 11, description: "진말숙 -> 진선미" },
{ id: "한승조", group: 12, description: "" },
{ id: "한승국", group: 12, description: "" },
{ id: "테이", group: 12, description: "승조의 형(한승국)이 14년 전에 데려온 개" },
{ id: "강한영", group: 13, description: "" },
{ id: "강한영 엄마", group: 13, description: "" },
{ id: "강한영 아빠", group: 13, description: "시청 공무원" },
{ id: "김혁현", group: 14, description: "" },
{ id: "천재소녀", group: 14, description: "외과 김태희" },
{ id: "배윤나", group: 15, description: "씽크홀에 빠짐" },
{ id: "찬주", group: 15, description: "" },
{ id: "베이글 가게 사장", group: 15, description: "" },
{ id: "베이글 가게 알바", group: 15, description: "" },
{ id: "규익", group: 15, description: "" },
{ id: "이호", group: 16, description: "1940년생, 감염내과 전문의" },
{ id: "이호 부인", group: 16, description: "미술 전공" },
{ id: "문영린", group: 17, description: "잘 우는 아이" },
{ id: "문영린 남자친구", group: 17, description: "" },
{ id: "조희락", group: 18, description: "희귀병 (베체트병) 환자, Didn't we? 가게 주인" },
{ id: "김의진", group: 19, description: "" },
{ id: "민희", group: 19, description: "" },
{ id: "재준", group: 19, description: "" },
{ id: "서진곤", group: 20, description: "" },
{ id: "서연모", group: 20, description: "" },
{ id: "서진곤 부인", group: 20, description: "" },
{ id: "권나은", group: 21, description: "" },
{ id: "권나은 선생님", group: 21, description: "" },
{ id: "홍우섭", group: 22, description: "" },
{ id: "박지혜", group: 22, description: "" },
{ id: "정지선", group: 23, description: "부동산 관련 업무" },
{ id: "정지은", group: 23, description: "" },
{ id: "오정빈", group: 24, description: "" },
{ id: "다운", group: 24, description: "" },
{ id: "오정빈 할머니", group: 24, description: "" },
{ id: "김인지", group: 25, description: "" },
{ id: "오수지", group: 25, description: "" },
{ id: "박현지", group: 25, description: "" },
{ id: "?지?", group: 25, description: "이름의 가운데 글자가 '지'" },
{ id: "고정우", group: 25, description: "패러글라이딩 사고로 사망" },
{ id: "오수지 연인", group: 25, description: "오수지가 57세일 때 고정우가 난기류를 만나 추락사한 지점에 같이 간 사람" },
{ id: "공운영", group: 26, description: "정리 결벽증" },
{ id: "인철", group: 26, description: "공운영 남편" },
{ id: "공운영 딸", group: 26, description: "중학생" },
{ id: "공운영 아들", group: 26, description: "초등학생" },
{ id: "스티브 코티앙", group: 27, description: "나이지리아 핸드볼 선수" },
{ id: "소현재", group: 27, description: "어린 의사, 영어를 잘 못 하는 의사" },
{ id: "이호", group: 27, description: "청진기도 사용하지 않는 할아버지 의사 (아마도 이호)" },
{ id: "아이작", group: 27, description: "무장단체의 총격으로 사망" },
{ id: "김한나", group: 28, description: "사서" },
{ id: "김한나 엄마", group: 28, description: "" },
{ id: "김한나 친구", group: 28, description: "한나가 책 추천해줌" },
{ id: "병원 기사님", group: 28, description: "가구 조립 도와줌" },
{ id: "박이삭", group: 29, description: "임상시험 참가 알바" },
{ id: "박이삭 엄마", group: 29, description: "영화만 보면 딴소리 함" },
{ id: "박이삭 선배", group: 29, description: "싫어하는 선배" },
{ id: "지지", group: 29, description: "한영의 룸메이트" },
{ id: "지현", group: 30, description: "재즈 베이스 연주자" },
{ id: "드러머", group: 30, description: "재즈 드럼 연주자" },
{ id: "피아노", group: 30, description: "재즈 피아노 연주자" },
{ id: "밴드 리더", group: 30, description: "재즈 밴드 리더" },
{ id: "최대환", group: 31, description: "전투기 조종사" },
{ id: "광 상사", group: 31, description: "'강'씨, 최대환을 괴롭힘" },
{ id: "군기교육대 대대장", group: 31, description: "군기교육대 대대장" },
{ id: "군기교육대 대대장 부인", group: 31, description: "대환의 초등학교 담임" },
{ id: "최대환 형", group: 31, description: "여행 경비를 마련해 줌" },
{ id: "최대환 후배", group: 31, description: "닥터 헬기 조종사 자리를 소개해 줌" },
{ id: "양혜련", group: 32, description: "" },
],
links: [
{ source: "송수정", target: "송수정 엄마", value: 1 },
{ source: "송수정", target: "송수정 남자친구", value: 1 },
{ source: "송수정 엄마", target: "송수정 남자친구", value: 1 },
{ source: "이기윤", target: "인턴 1", value: 1 },
{ source: "이기윤", target: "승희", value: 1 },
{ source: "권혜정", target: "정형외과 교수", value: 1 },
{ source: "권혜정", target: "인턴 2", value: 1 },
{ source: "조양선", target: "조양선 오빠", value: 1 },
{ source: "조양선", target: "성식", value: 1 },
{ source: "조양선", target: "승희", value: 1 },
{ source: "김성진", target: "강한정", value: 1 },
{ source: "최애선", target: "최애선 첫째 며느리", value: 1 },
// { source: "최애선", target: "최애선 둘째 아들", value: 1 },
{ source: "최애선", target: "윤나", value: 1 },
// { source: "윤나", target: "최애선 둘째 아들", value: 1 },
{ source: "임대열", target: "인턴 3", value: 1 },
{ source: "임대열", target: "임대열 부인", value: 1 },
{ source: "임대열", target: "임대열 자식 1", value: 1 },
{ source: "임대열", target: "임대열 자식 2", value: 1 },
{ source: "임대열", target: "레지던트 1", value: 1 },
{ source: "장유라", target: "오헌영", value: 1 },
{ source: "장유라", target: "오정빈", value: 1 },
{ source: "오헌영", target: "오정빈", value: 1 },
{ source: "장유라", target: "화물연대 사람들", value: 1 },
{ source: "이환의", target: "윤나", value: 1, relationship: "부인" },
{ source: "이환의", target: "최애선", value: 1, relationship: "장모님" },
{ source: "유채원", target: "환자 1", value: 1, relationship: "수술실 환자" },
{ source: "유채원", target: "내과 교수 1", value: 1, relationship: "동료 의사" },
{ source: "내과 교수 1", target: "환자 1", value: 1, relationship: "수술실 환자" },
{ source: "문우남", target: "진선미", value: 1, relationship: "부인" },
{ source: "한승조", target: "한승국", value: 1, relationship: "형" },
{ source: "한승조", target: "테이", value: 1, relationship: "형이 데려온 개" },
{ source: "한승국", target: "테이", value: 1, relationship: "데려온 개" },
{ source: "강한영", target: "강한정", value: 1, relationship: "동생" },
{ source: "강한영", target: "강한영 엄마", value: 1, relationship: "엄마" },
{ source: "강한정", target: "강한영 엄마", value: 1, relationship: "엄마" },
{ source: "강한영", target: "강한영 아빠", value: 1, relationship: "아빠" },
{ source: "강한정", target: "강한영 아빠", value: 1, relationship: "아빠" },
{ source: "김혁현", target: "천재소녀", value: 1, relationship: "천재소녀에게 호감 있음" },
{ source: "배윤나", target: "찬주", value: 1, relationship: "선배" },
{ source: "배윤나", target: "규익", value: 1, relationship: "후배(맞는지 확인 필요)" },
{ source: "배윤나", target: "베이글 가게 사장", value: 1, relationship: "" },
{ source: "배윤나", target: "베이글 가게 알바", value: 1, relationship: "베이글 가게 알바 1" },
{ source: "배윤나", target: "승희", value: 1, relationship: "베이글 가게 알바 2" },
{ source: "이호", target: "이호 부인", value: 1, relationship: "부인" },
{ source: "문영린", target: "문우남", value: 1, relationship: "아빠" },
{ source: "문영린", target: "진선미", value: 1, relationship: "새엄마" },
{ source: "문영린", target: "문영린 남자친구", value: 1, relationship: "학교 선배, 남자친구" },
{ source: "조희락", target: "이호", value: 1, relationship: "(아마도) Didn't we 가게 손님" },
{ source: "김의진", target: "민희", value: 1, relationship: "친구" },
{ source: "민희", target: "재준", value: 1, relationship: "아들" },
{ source: "서진곤", target: "서연모", value: 1, relationship: "아들" },
{ source: "서진곤", target: "서진곤 부인", value: 1, relationship: "부인" },
{ source: "서연모", target: "서진곤 부인", value: 1, relationship: "엄마" },
{ source: "권나은", target: "승희", value: 1, relationship: "친구" },
{ source: "권나은", target: "권나은 선생님", value: 1, relationship: "학교 선생님" },
{ source: "홍우섭", target: "박지혜", value: 1, relationship: "소개팅으로 만난 여자" },
{ source: "정지선", target: "정지은", value: 1, relationship: "동생" },
{ source: "오정빈", target: "다운", value: 1, relationship: "친구" },
{ source: "오정빈", target: "오정빈 할머니", value: 1, relationship: "친구" },
{ source: "김인지", target: "오수지", value: 1, relationship: "룸메이트" },
{ source: "김인지", target: "박현지", value: 1, relationship: "룸메이트" },
{ source: "오수지", target: "박현지", value: 1, relationship: "룸메이트" },
{ source: "김인지", target: "고정우", value: 1, relationship: "섹스 파트너" },
{ source: "오수지", target: "고정우", value: 1, relationship: "섹스 파트너" },
{ source: "박현지", target: "고정우", value: 1, relationship: "섹스 파트너" },
{ source: "오수지", target: "오수지 연인", value: 1, relationship: "연인" },
{ source: "공운영", target: "인철", value: 1, relationship: "남편" },
{ source: "공운영", target: "공운영 딸", value: 1, relationship: "딸" },
{ source: "공운영", target: "공운영 아들", value: 1, relationship: "아들" },
{ source: "스티브 코티앙", target: "소현재", value: 1, relationship: "담당 의사" },
{ source: "스티브 코티앙", target: "이호", value: 1, relationship: "담당 의사" },
{ source: "스티브 코티앙", target: "아이작", value: 1, relationship: "사촌" },
{ source: "김한나", target: "김한나 엄마", value: 1, relationship: "엄마" },
{ source: "김한나", target: "김한나 친구", value: 1, relationship: "친구" },
{ source: "김한나", target: "병원 기사님", value: 1, relationship: "직장 동료" },
{ source: "박이삭", target: "박이삭 엄마", value: 1, relationship: "엄마" },
{ source: "박이삭", target: "박이삭 선배", value: 1, relationship: "대학 선배" },
{ source: "박이삭", target: "김한나", value: 1, relationship: "호감 있음" },
{ source: "박이삭", target: "강한영", value: 1, relationship: "친구" },
{ source: "박이삭", target: "지지", value: 1, relationship: "호감 있음" },
{ source: "강한영", target: "지지", value: 1, relationship: "룸메이트" },
{ source: "지현", target: "드러머", value: 1, relationship: "재즈 밴드 동료" },
{ source: "지현", target: "피아노", value: 1, relationship: "재즈 밴드 동료" },
{ source: "지현", target: "밴드 리더", value: 1, relationship: "재즈 밴드 동료" },
{ source: "드러머", target: "피아노", value: 1, relationship: "재즈 밴드 동료" },
{ source: "드러머", target: "밴드 리더", value: 1, relationship: "재즈 밴드 동료" },
{ source: "피아노", target: "밴드 리더", value: 1, relationship: "재즈 밴드 동료" },
{ source: "지현", target: "조희락", value: 1, relationship: "친구" },
{ source: "최대환", target: "광 상사", value: 1, relationship: "직장 상사" },
{ source: "최대환", target: "군기교육대 대대장", value: 1, relationship: "직장 상사" },
{ source: "최대환", target: "군기교육대 대대장 부인", value: 1, relationship: "초등학교 담임" },
{ source: "최대환", target: "최대환 형", value: 1, relationship: "형" },
{ source: "최대환", target: "최대환 후배", value: 1, relationship: "직장 후배" },
{ source: "군기교육대 대대장", target: "군기교육대 대대장 부인", value: 1, relationship: "부인" },
// { source: "", target: "", value: 1, relationship: "" },
]
}
const height = (data.nodes.length - 1) * step + margin.top + margin.bottom;
const nodes = data.nodes.map(({ id, group }) => ({
id,
sourceLinks: [],
targetLinks: [],
group
}));
const nodeById = new Map(nodes.map(d => [d.id, d]));
const links = data.links.map(({ source, target, value }) => ({
source: nodeById.get(source),
target: nodeById.get(target),
value
}));
for (const link of links) {
const { source, target, value } = link;
console.log(source, target);
source.sourceLinks.push(link);
target.targetLinks.push(link);
}
const y = d3.scalePoint(
nodes.map(d => d.id).sort(d3.ascending),
[margin.top, height - margin.bottom]);
const color = d3.scaleOrdinal(
nodes.map(d => d.group).sort(d3.ascending),
d3.schemeCategory10);
const label = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 11)
.attr("text-anchor", "end")
.selectAll("g")
.data(nodes)
.join("g")
.attr("transform", d => `translate(${margin.left},${d.y = y(d.id)})`)
.call(g => g.append("text")
.attr("x", -6)
.attr("dy", "0.35em")
.attr("fill", d => d3.lab(color(d.group)).darker(2))
.text(d => d.id))
.call(g => g.append("circle")
.attr("r", 3)
.attr("fill", d => color(d.group)));
const path = svg.insert("g", "*")
.attr("fill", "none")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(links)
.join("path")
.attr("stroke", d => d.source.group === d.target.group ? color(d.source.group) : "#aaa")
.attr("d", arc);
const overlay = svg.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("rect")
.data(nodes)
.join("rect")
.attr("width", margin.left + 40)
.attr("height", step)
.attr("y", d => y(d.id) - step / 2)
.on("mouseover", d => {
svg.classed("hover", true);
label.classed("primary", n => n === d);
label.classed("secondary", n => n.sourceLinks.some(l => l.target === d) || n.targetLinks.some(l => l.source === d));
path.classed("primary", l => l.source === d || l.target === d).filter(".primary").raise();
})
.on("mouseout", d => {
svg.classed("hover", false);
label.classed("primary", false);
label.classed("secondary", false);
path.classed("primary", false).order();
});
// function update() {
// y.domain(graph.nodes.sort(viewof order.value).map(d => d.id));
// const t = svg.transition()
// .duration(750);
// label.transition(t)
// .delay((d, i) => i * 20)
// .attrTween("transform", d => {
// const i = d3.interpolateNumber(d.y, y(d.id));
// return t => `translate(${margin.left},${d.y = i(t)})`;
// });
// path.transition(t)
// .duration(750 + graph.nodes.length * 20)
// .attrTween("d", d => () => arc(d));
// overlay.transition(t)
// .delay((d, i) => i * 20)
// .attr("y", d => y(d.id) - step / 2);
// }
// viewof order.addEventListener("input", update);
// invalidation.then(() => viewof order.removeEventListener("input", update));
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment