Created
May 11, 2020 13:42
-
-
Save suminb/63f8a45436864e7407129e713e5641ba to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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