Skip to content

Commit 51d3dfe

Browse files
committed
add MCV example in TypeScript
1 parent e55963e commit 51d3dfe

4 files changed

Lines changed: 376 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,7 @@ One can specify the type in more detail by specifing the generic argument of the
11721172
---
11731173

11741174
Barchart final results in TypeScript [barchart07_final_ts.ts](examples/barchart07_final_ts.ts) [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/gObqdEG)
1175+
MCV final results in TypeScript [mcv06_final_ts.ts](examples/mcv06_final_ts.ts) [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/pojoNNL)
11751176

11761177
### Hints
11771178

examples/mcv06_final_ts.html

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<title>Student's First Multiple Coordinated View</title>
7+
<meta name="description" content="Student's First Multiple Coordinated View" />
8+
<meta name="viewport" content="width=device-width, initial-scale=1" />
9+
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
10+
<script src="../web_modules/d3/dist/d3.js"></script>
11+
<link href="./mvc.css" rel="stylesheet" />
12+
<style>
13+
rect {
14+
fill: steelblue;
15+
fill-opacity: 0.8;
16+
}
17+
18+
rect:hover {
19+
fill-opacity: 1;
20+
}
21+
22+
path {
23+
fill-opacity: 0.8;
24+
}
25+
26+
.selected,
27+
path:hover {
28+
fill-opacity: 1;
29+
}
30+
31+
.axis {
32+
font-size: smaller;
33+
}
34+
35+
main {
36+
display: flex;
37+
flex-wrap: wrap;
38+
}
39+
40+
h3 {
41+
text-align: center;
42+
}
43+
</style>
44+
</head>
45+
46+
<body>
47+
<h1>Student's First Multiple Coordinated View</h1>
48+
49+
<div>
50+
<label for="passenger-class"><strong>Passenger Class:</strong></label>
51+
<select id="passenger-class">
52+
<option value="" selected>All Classes</option>
53+
<option value="1">First Class</option>
54+
<option value="2">2nd Class</option>
55+
<option value="3">3rd Class</option>
56+
</select>
57+
<strong>Selected Gender: </strong>
58+
<span id="selectedSex"></span>
59+
<strong>Selected Survived: </strong>
60+
<span id="selectedSurvived"></span>
61+
</div>
62+
63+
<main>
64+
<section>
65+
<h3>Gender Distribution</h3>
66+
<svg id="sex"></svg>
67+
</section>
68+
<section>
69+
<h3>Age Histogram</h3>
70+
<svg id="age"></svg>
71+
</section>
72+
<section>
73+
<h3>Fare Histogram</h3>
74+
<svg id="fare"></svg>
75+
</section>
76+
<section>
77+
<h3>Survived Distribution</h3>
78+
<svg id="survived"></svg>
79+
</section>
80+
</main>
81+
82+
<script src="./mcv06_final_ts.js"></script>
83+
</body>
84+
</html>

examples/mcv06_final_ts.ts

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
declare type Sex = "female" | "male";
2+
declare type Survival = "0" | "1";
3+
4+
interface IPerson {
5+
age: number;
6+
fare: number;
7+
pclass: string;
8+
survived: Survival;
9+
sex: Sex;
10+
}
11+
12+
interface IPersonGroup {
13+
key: string;
14+
values: IPerson[];
15+
}
16+
17+
interface IState {
18+
data: IPerson[];
19+
passengerClass: string;
20+
selectedSex: null | Sex;
21+
selectedSurvived: null | Survival;
22+
}
23+
24+
declare type SimpleArcDatum = {
25+
startAngle: number;
26+
endAngle: number;
27+
padAngle: number;
28+
};
29+
30+
const state: IState = {
31+
data: [],
32+
passengerClass: "",
33+
selectedSex: null,
34+
selectedSurvived: null,
35+
};
36+
37+
function createHistogram(svgSelector: string) {
38+
const margin = {
39+
top: 40,
40+
bottom: 10,
41+
left: 120,
42+
right: 20,
43+
};
44+
const width = 600 - margin.left - margin.right;
45+
const height = 400 - margin.top - margin.bottom;
46+
47+
// Creates sources <svg> element
48+
const svg = d3
49+
.select(svgSelector)
50+
.attr("width", width + margin.left + margin.right)
51+
.attr("height", height + margin.top + margin.bottom);
52+
53+
// Group used to enforce margin
54+
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
55+
56+
// Scales setup
57+
const xscale = d3.scaleLinear().range([0, width]);
58+
const yscale = d3.scaleLinear().range([0, height]);
59+
60+
// Axis setup
61+
const xaxis = d3.axisTop(xscale);
62+
const g_xaxis = g.append("g").attr("class", "x axis");
63+
const yaxis = d3.axisLeft(yscale);
64+
const g_yaxis = g.append("g").attr("class", "y axis");
65+
66+
function update(new_data: d3.Bin<IPerson, number>[]) {
67+
//update the scales
68+
xscale.domain([0, d3.max(new_data, (d) => d.length)!]);
69+
yscale.domain([new_data[0].x0!, new_data[new_data.length - 1].x1!]);
70+
//render the axis
71+
g_xaxis.transition().call(xaxis);
72+
g_yaxis.transition().call(yaxis);
73+
74+
// Render the chart with new data
75+
76+
// DATA JOIN
77+
const rect = g
78+
.selectAll("rect")
79+
.data(new_data)
80+
.join(
81+
(enter) => {
82+
// ENTER
83+
// new elements
84+
const rect_enter = enter
85+
.append("rect")
86+
.attr("x", 0) //set intelligent default values for animation
87+
.attr("y", 0)
88+
.attr("width", 0)
89+
.attr("height", 0);
90+
rect_enter.append("title");
91+
return rect_enter;
92+
},
93+
// UPDATE
94+
// update existing elements
95+
(update) => update,
96+
// EXIT
97+
// elements that aren't associated with data
98+
(exit) => exit.remove()
99+
);
100+
101+
// ENTER + UPDATE
102+
// both old and new elements
103+
rect
104+
.transition()
105+
.attr("height", (d) => yscale(d.x1!) - yscale(d.x0!) - 2)
106+
.attr("width", (d) => xscale(d.length))
107+
.attr("y", (d) => yscale(d.x0!) + 1);
108+
109+
rect.select("title").text((d) => `${d.x0}: ${d.length}`);
110+
}
111+
112+
return update;
113+
}
114+
115+
function createPieChart(
116+
svgSelector: string,
117+
stateAttr: "selectedSex" | "selectedSurvived",
118+
colorScheme: ReadonlyArray<string>
119+
) {
120+
const margin = 10;
121+
const radius = 100;
122+
123+
// Creates sources <svg> element
124+
const svg = d3
125+
.select(svgSelector)
126+
.attr("width", radius * 2 + margin * 2)
127+
.attr("height", radius * 2 + margin * 2);
128+
129+
// Group used to enforce margin
130+
const g = svg.append("g").attr("transform", `translate(${radius + margin},${radius + margin})`);
131+
132+
const pie = d3
133+
.pie<IPersonGroup>()
134+
.value((d) => d.values.length)
135+
.sortValues(null)
136+
.sort(null);
137+
const arc = d3.arc<SimpleArcDatum>().outerRadius(radius).innerRadius(0);
138+
139+
const noSlice: SimpleArcDatum[] = [
140+
{ startAngle: 0, endAngle: Math.PI * 2, padAngle: 0 },
141+
{ startAngle: 0, endAngle: 0, padAngle: 0 },
142+
];
143+
144+
const cscale = d3.scaleOrdinal(colorScheme);
145+
146+
function update(new_data: IPersonGroup[]) {
147+
const pied = pie(new_data);
148+
// Render the chart with new data
149+
150+
cscale.domain(new_data.map((d) => d.key));
151+
152+
// DATA JOIN
153+
const old = g.selectAll<SVGPathElement, d3.PieArcDatum<IPersonGroup>>("path").data();
154+
155+
function tweenArc(d: d3.PieArcDatum<IPersonGroup>, i: number) {
156+
const interpolate = d3.interpolateObject(old[i], d);
157+
return (t: number) => arc(interpolate(t))!;
158+
}
159+
160+
// DATA JOIN
161+
const path = g
162+
.selectAll("path")
163+
.data(pied, (d) => (d as d3.PieArcDatum<IPersonGroup>).data.key)
164+
.join(
165+
// ENTER
166+
// new elements
167+
(enter) => {
168+
const path_enter = enter
169+
.append("path")
170+
.attr("d", (_d, i) => arc(noSlice[i]))
171+
.on("click", (d) => {
172+
if (state[stateAttr] === d.data.key) {
173+
state[stateAttr] = null;
174+
} else {
175+
state[stateAttr] = d.data.key as any;
176+
}
177+
updateApp();
178+
});
179+
path_enter.append("title");
180+
return path_enter;
181+
},
182+
(update) => update,
183+
(exit) => exit.transition().attrTween("d", tweenArc).remove()
184+
);
185+
186+
// ENTER + UPDATE
187+
// both old and new elements
188+
path
189+
.classed("selected", (d) => d.data.key === state.selectedSex)
190+
.transition()
191+
.attrTween("d", tweenArc)
192+
.style("fill", (d) => cscale(d.data.key));
193+
194+
path.select("title").text((d) => `${d.data.key}: ${d.data.values.length}`);
195+
}
196+
return update;
197+
}
198+
199+
/////////////////////////
200+
201+
const ageHistogram = createHistogram("#age");
202+
const sexPieChart = createPieChart("#sex", "selectedSex", d3.schemeSet3);
203+
const fareHistogram = createHistogram("#fare");
204+
const survivedPieChart = createPieChart("#survived", "selectedSurvived", d3.schemeSet3.slice(2));
205+
206+
function filterData() {
207+
return state.data.filter((d) => {
208+
if (state.passengerClass && d.pclass !== state.passengerClass) {
209+
return false;
210+
}
211+
if (state.selectedSex && d.sex !== state.selectedSex) {
212+
return false;
213+
}
214+
if (state.selectedSurvived && d.survived !== state.selectedSurvived) {
215+
return false;
216+
}
217+
return true;
218+
});
219+
}
220+
221+
function wrangleData(filtered: IPerson[]) {
222+
const ageHistogram = d3
223+
.histogram<IPerson, number>()
224+
.domain([0, 100])
225+
.thresholds(10)
226+
.value((d) => d.age);
227+
228+
const ageHistogramData = ageHistogram(filtered);
229+
230+
// always the two categories
231+
const sexPieData = (["female", "male"] as Sex[]).map((key) => ({
232+
key,
233+
values: filtered.filter((d) => d.sex === key),
234+
}));
235+
236+
const fareHistogram = d3
237+
.histogram<IPerson, number>()
238+
.domain([0, d3.max(filtered, (d) => d.fare)!])
239+
.value((d) => d.fare);
240+
241+
const fareHistogramData = fareHistogram(filtered);
242+
243+
// always the two categories
244+
const survivedPieData = (["0", "1"] as Survival[]).map((key) => ({
245+
key,
246+
values: filtered.filter((d) => d.survived === key),
247+
}));
248+
249+
return {
250+
ageHistogramData,
251+
sexPieData,
252+
fareHistogramData,
253+
survivedPieData,
254+
};
255+
}
256+
257+
function updateApp() {
258+
const filtered = filterData();
259+
260+
const { ageHistogramData, sexPieData, fareHistogramData, survivedPieData } = wrangleData(filtered);
261+
ageHistogram(ageHistogramData);
262+
sexPieChart(sexPieData);
263+
fareHistogram(fareHistogramData);
264+
survivedPieChart(survivedPieData);
265+
266+
d3.select("#selectedSex").text(state.selectedSex || "None");
267+
d3.select("#selectedSurvived").text(state.selectedSurvived || "None");
268+
}
269+
270+
d3.csv<keyof IPerson>("titanic3.csv").then((parsed) => {
271+
state.data = parsed.map((row) => ({
272+
pclass: row.pclass!,
273+
sex: row.sex as Sex,
274+
survived: row.survived as Survival,
275+
age: Number.parseInt(row.age!, 10),
276+
fare: Number.parseFloat(row.fare!),
277+
}));
278+
279+
updateApp();
280+
});
281+
282+
//interactivity
283+
d3.select<HTMLInputElement, unknown>("#passenger-class").on("change", function () {
284+
const selected = this.value;
285+
state.passengerClass = selected;
286+
updateApp();
287+
});

index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ <h2>TypeScript Examples</h2>
118118
<a href="./examples/barchart_final_ts.html">Bar Chart</a>
119119
<a href="https://codepen.io/sgratzl/pen/gObqdEG" target="_blank" rel="noopener noreferer">(@Codepen.io)</a>
120120
</li>
121+
<li>
122+
<a href="./examples/mcv06_final_ts.html">MCV Example</a>
123+
<a href="https://codepen.io/sgratzl/pen/pojoNNL" target="_blank" rel="noopener noreferer">(@Codepen.io)</a>
124+
</li>
121125
</ul>
122126
</section>
123127
</body>

0 commit comments

Comments
 (0)