Skip to content

Commit 6d7cc8f

Browse files
committed
add chart (d3.js custom)
1 parent 1271864 commit 6d7cc8f

File tree

6 files changed

+340
-11
lines changed

6 files changed

+340
-11
lines changed

index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
<meta charset="utf-8">
66
<title>WebActivity Time Tracker</title>
77
<link href="style/webact.css" rel="stylesheet" />
8+
<link href="style/chart.css" rel="stylesheet" />
9+
<script src="scripts/chart/d3.v4.min.js"></script>
10+
<script src="scripts/chart/chart-core.js"></script>
811
<script src="scripts/storage.js"></script>
912
<script src="scripts/common.js"></script>
1013
<script src="scripts/webact.js"></script>
1114
</head>
1215

1316
<body>
17+
<div id="chart"></div>
1418
<hr>
1519
<div class="list-of-site" id="resultTable"></div>
1620
<hr>

scripts/chart/chart-core.js

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
function donutChart() {
2+
var width,
3+
height,
4+
margin = {top: 10, right: 10, bottom: 10, left: 10},
5+
colour = d3.scaleOrdinal(d3.schemeCategory20), // colour scheme
6+
variable, // value in data that will dictate proportions on chart
7+
category, // compare data by
8+
padAngle, // effectively dictates the gap between slices
9+
floatFormat = d3.format('.4r'),
10+
cornerRadius, // sets how rounded the corners are on each slice
11+
percentFormat = d3.format(',.2%');
12+
13+
function chart(selection){
14+
selection.each(function(data) {
15+
// generate chart
16+
17+
// ===========================================================================================
18+
// Set up constructors for making donut. See https://github.com/d3/d3-shape/blob/master/README.md
19+
var radius = 140;
20+
21+
// creates a new pie generator
22+
var pie = d3.pie()
23+
.value(function(d) { return floatFormat(d[variable]); })
24+
.sort(null);
25+
26+
// contructs and arc generator. This will be used for the donut. The difference between outer and inner
27+
// radius will dictate the thickness of the donut
28+
var arc = d3.arc()
29+
.outerRadius(radius * 0.9)
30+
.innerRadius(radius * 0.6)
31+
.cornerRadius(cornerRadius)
32+
.padAngle(padAngle);
33+
34+
// this arc is used for aligning the text labels
35+
var outerArc = d3.arc()
36+
.outerRadius(radius * 0.9)
37+
.innerRadius(radius * 0.9);
38+
// ===========================================================================================
39+
40+
// ===========================================================================================
41+
// append the svg object to the selection
42+
var svg = selection.append('svg')
43+
.attr('width', width + margin.left + margin.right)
44+
.attr('height', height + margin.top + margin.bottom)
45+
.append('g')
46+
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
47+
// ===========================================================================================
48+
49+
// ===========================================================================================
50+
// g elements to keep elements within svg modular
51+
svg.append('g').attr('class', 'slices');
52+
svg.append('g').attr('class', 'labelName');
53+
svg.append('g').attr('class', 'lines');
54+
// ===========================================================================================
55+
56+
// ===========================================================================================
57+
// add and colour the donut slices
58+
var path = svg.select('.slices')
59+
.datum(data).selectAll('path')
60+
.data(pie)
61+
.enter().append('path')
62+
.attr('fill', function(d) { return colour(d.data[category]); })
63+
.attr('d', arc);
64+
// ===========================================================================================
65+
66+
// ===========================================================================================
67+
// add text labels
68+
var label = svg.select('.labelName').selectAll('text')
69+
.data(pie)
70+
.enter().append('text')
71+
.attr('dy', '.35em')
72+
.html(function(d) {
73+
// add "key: value" for given category. Number inside tspan is bolded in stylesheet.
74+
return ' <tspan class="siteName">' + d.data[category] + ' </tspan>';
75+
})
76+
.attr('transform', function(d) {
77+
78+
// effectively computes the centre of the slice.
79+
// see https://github.com/d3/d3-shape/blob/master/README.md#arc_centroid
80+
var pos = outerArc.centroid(d);
81+
82+
// changes the point to be on left or right depending on where label is.
83+
pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
84+
return 'translate(' + pos + ')';
85+
})
86+
.style('text-anchor', function(d) {
87+
// if slice centre is on the left, anchor text to start, otherwise anchor to end
88+
return (midAngle(d)) < Math.PI ? 'start' : 'end';
89+
});
90+
// ===========================================================================================
91+
92+
// ===========================================================================================
93+
// add lines connecting labels to slice. A polyline creates straight lines connecting several points
94+
var polyline = svg.select('.lines')
95+
.selectAll('polyline')
96+
.data(pie)
97+
.enter().append('polyline')
98+
.attr('points', function(d) {
99+
100+
// see label transform function for explanations of these three lines.
101+
var pos = outerArc.centroid(d);
102+
pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
103+
return [arc.centroid(d), outerArc.centroid(d), pos]
104+
});
105+
// ===========================================================================================
106+
107+
// ===========================================================================================
108+
// add tooltip to mouse events on slices and labels
109+
d3.selectAll('.labelName text, .slices path').call(toolTip);
110+
// ===========================================================================================
111+
112+
// ===========================================================================================
113+
// Functions
114+
115+
// calculates the angle for the middle of a slice
116+
function midAngle(d) { return d.startAngle + (d.endAngle - d.startAngle) / 2; }
117+
118+
// function that creates and adds the tool tip to a selected element
119+
function toolTip(selection) {
120+
121+
// add tooltip (svg circle element) when mouse enters label or slice
122+
selection.on('mouseenter', function (data) {
123+
124+
svg.append('text')
125+
.attr('class', 'toolCircle')
126+
.attr('dy', -15) // hard-coded. can adjust this to adjust text vertical alignment in tooltip
127+
.html(toolTipHTML(data)) // add text to the circle.
128+
.style('font-size', '.9em')
129+
.style('text-anchor', 'middle'); // centres text in tooltip
130+
131+
svg.append('circle')
132+
.attr('class', 'toolCircle')
133+
.attr('r', radius * 0.55) // radius of tooltip circle
134+
.style('fill', colour(data.data[category])) // colour based on category mouse is over
135+
.style('fill-opacity', 0.35);
136+
137+
});
138+
139+
// remove the tooltip when mouse leaves the slice/label
140+
selection.on('mouseout', function () {
141+
d3.selectAll('.toolCircle').remove();
142+
});
143+
}
144+
145+
// function to create the HTML string for the tool tip. Loops through each key in data object
146+
// and returns the html string key: value
147+
function toolTipHTML(data) {
148+
149+
var tip = '',
150+
i = 0;
151+
152+
for (var key in data.data) {
153+
154+
// if value is a number, format it as a percentage
155+
var value = (!isNaN(parseFloat(data.data[key]))) ? percentFormat(data.data[key]) : data.data[key];
156+
if (key === 'summary')
157+
value = convertSummaryTimeToString(data.data[key]);
158+
var className = '';
159+
if (key === 'percentage')
160+
className = 'class="percentageValue"';
161+
162+
// leave off 'dy' attr for first tspan so the 'dy' attr on text element works. The 'dy' attr on
163+
// tspan effectively imitates a line break.
164+
if (i === 0) tip += '<tspan x="0">' + value + '</tspan>';
165+
else tip += '<tspan x="0" dy="1.2em"' + className + '>' + value + '</tspan>';
166+
i++;
167+
}
168+
169+
return tip;
170+
}
171+
// ===========================================================================================
172+
173+
});
174+
}
175+
176+
// getter and setter functions. See Mike Bostocks post "Towards Reusable Charts" for a tutorial on how this works.
177+
chart.width = function(value) {
178+
if (!arguments.length) return width;
179+
width = value;
180+
return chart;
181+
};
182+
183+
chart.height = function(value) {
184+
if (!arguments.length) return height;
185+
height = value;
186+
return chart;
187+
};
188+
189+
chart.margin = function(value) {
190+
if (!arguments.length) return margin;
191+
margin = value;
192+
return chart;
193+
};
194+
195+
chart.radius = function(value) {
196+
if (!arguments.length) return radius;
197+
radius = value;
198+
return chart;
199+
};
200+
201+
chart.padAngle = function(value) {
202+
if (!arguments.length) return padAngle;
203+
padAngle = value;
204+
return chart;
205+
};
206+
207+
chart.cornerRadius = function(value) {
208+
if (!arguments.length) return cornerRadius;
209+
cornerRadius = value;
210+
return chart;
211+
};
212+
213+
chart.colour = function(value) {
214+
if (!arguments.length) return colour;
215+
colour = value;
216+
return chart;
217+
};
218+
219+
chart.variable = function(value) {
220+
if (!arguments.length) return variable;
221+
variable = value;
222+
return chart;
223+
};
224+
225+
chart.category = function(value) {
226+
if (!arguments.length) return category;
227+
category = value;
228+
return chart;
229+
};
230+
231+
return chart;
232+
}

scripts/chart/d3.v4.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/webact.js

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function getTabsFromStorage(tabs) {
2020

2121
var currentTab = getCurrentTab();
2222

23+
var tabsForChart = [];
2324
for (var i = 0; i < tabs.length; i++) {
2425
var div = document.createElement('div');
2526
div.classList.add('inline-flex');
@@ -32,13 +33,17 @@ function getTabsFromStorage(tabs) {
3233
var spanUrl = document.createElement('span');
3334
spanUrl.classList.add('span-url');
3435
spanUrl.innerText = tabs[i].url;
35-
if (tabs[i].url == currentTab){
36+
if (tabs[i].url == currentTab) {
3637
spanUrl.classList.add('span-active-url');
3738
}
3839

3940
var spanPercentage = document.createElement('span');
4041
spanPercentage.classList.add('span-percentage');
41-
spanPercentage.innerText = setPercentage(tabs[i].summaryTime);
42+
spanPercentage.innerText = getPercentage(tabs[i].summaryTime);
43+
44+
if (i <= 5)
45+
addTabForChart(tabsForChart, tabs[i].url, tabs[i].summaryTime);
46+
else addTabOthersForChart(tabsForChart, tabs[i].summaryTime);
4247

4348
var spanTime = document.createElement('span');
4449
spanTime.classList.add('span-time');
@@ -50,6 +55,8 @@ function getTabsFromStorage(tabs) {
5055
div.appendChild(spanTime);
5156
table.appendChild(div);
5257
}
58+
59+
drawChart(tabsForChart);
5360
}
5461

5562
function setTotalTime(tabs) {
@@ -60,10 +67,55 @@ function setTotalTime(tabs) {
6067
return total;
6168
}
6269

63-
function setPercentage(time){
70+
function getPercentage(time) {
6471
return ((time / totalTime) * 100).toFixed(2) + '%';
6572
}
6673

67-
function getCurrentTab(){
74+
function getPercentageForChart(time) {
75+
return ((time / totalTime) * 100).toFixed(2) / 100;
76+
}
77+
78+
function getCurrentTab() {
6879
return chrome.extension.getBackgroundPage().currentTab;
80+
}
81+
82+
function addTabForChart(tabsForChart, url, time) {
83+
tabsForChart.push(
84+
{
85+
'url': url,
86+
'percentage': getPercentageForChart(time),
87+
'summary': time
88+
}
89+
);
90+
}
91+
92+
function addTabOthersForChart(tabsForChart, summaryTime) {
93+
var tab = tabsForChart.find(x => x.url == 'Others');
94+
if (tab === undefined) {
95+
tabsForChart.push(
96+
{
97+
'url': 'Others',
98+
'percentage': getPercentageForChart(summaryTime),
99+
'summary': summaryTime
100+
}
101+
);
102+
}
103+
else {
104+
tab['summary'] = tab['summary'] + summaryTime;
105+
tab['percentage'] = getPercentageForChart(tab['summary']);
106+
}
107+
}
108+
109+
function drawChart(tabs) {
110+
var donut = donutChart()
111+
.width(480)
112+
.height(280)
113+
.cornerRadius(5) // sets how rounded the corners are on each slice
114+
.padAngle(0.020) // effectively dictates the gap between slices
115+
.variable('percentage')
116+
.category('url');
117+
118+
d3.select('#chart')
119+
.datum(tabs) // bind data to the div
120+
.call(donut); // draw chart in div
69121
}

style/chart.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* Add shadow effect to chart. If you don't like it, get rid of it. */
2+
svg {
3+
-webkit-filter: drop-shadow( 0px 3px 3px rgba(0,0,0,.3) );
4+
filter: drop-shadow( 0px 3px 3px rgba(0,0,0,.25) );
5+
}
6+
7+
/*Styling for the lines connecting the labels to the slices*/
8+
polyline{
9+
opacity: .3;
10+
stroke: black;
11+
stroke-width: 2px;
12+
fill: none;
13+
}
14+
15+
/* Make the percentage on the text labels bold*/
16+
.labelName tspan {
17+
font-style: normal;
18+
font-weight: 700;
19+
}
20+
21+
/* In biology we generally italicise species names. */
22+
.labelName {
23+
font-size: 1.1em;
24+
}
25+
26+
tspan{
27+
font-size: 1.2em;
28+
font-weight: 600;
29+
}
30+
31+
.siteName{
32+
font-size: 1em !important;
33+
font-weight: 400 !important;
34+
}
35+
36+
.percentageValue{
37+
font-size: 1.8em !important;
38+
font-weight: 600 !important;
39+
}

0 commit comments

Comments
 (0)