Commit 6c51f5770678962a71a1ebb2719d6ff3248f98b6
Exists in
master
and in
4 other branches
Projects Stats Chart
Showing
8 changed files
with
208 additions
and
60 deletions
Show diff stats
CHANGELOG.md
@@ -24,6 +24,10 @@ or major refactoring improvments. | @@ -24,6 +24,10 @@ or major refactoring improvments. | ||
24 | 24 | ||
25 | ## Unreleased | 25 | ## Unreleased |
26 | 26 | ||
27 | +## [0.3.pre-13] - 2021-05-19 - Projects stats | ||
28 | +### New | ||
29 | +Projects stats charts | ||
30 | + | ||
27 | ## [0.3.pre-12] - 2021-05-19 - Enhance Front end | 31 | ## [0.3.pre-12] - 2021-05-19 - Enhance Front end |
28 | ### Changed | 32 | ### Changed |
29 | Speed up loading | 33 | Speed up loading |
VERSION.txt
app/db_mgr.py
@@ -202,6 +202,28 @@ def charges_by_project_stacked(project_id, category="service"): | @@ -202,6 +202,28 @@ def charges_by_project_stacked(project_id, category="service"): | ||
202 | return all_charges | 202 | return all_charges |
203 | 203 | ||
204 | 204 | ||
205 | +def charges_for_projects_stacked(): | ||
206 | + sql_req = """ | ||
207 | + select p.name, sum(charge_rate) as tot_charg | ||
208 | + from charge | ||
209 | + join project p on p.id = charge.project_id | ||
210 | + where period_id = {} | ||
211 | + group by project_id | ||
212 | + """ | ||
213 | + projects_req = "select name from project order by id" | ||
214 | + projects_names = [name for (name,) in db.session.execute(projects_req)] | ||
215 | + headers = ['period'] + projects_names | ||
216 | + all_charges = [headers] | ||
217 | + for (period_id, period_name) in db.session.execute("select id, name from period order by id"): | ||
218 | + project_charges = [period_name] | ||
219 | + charges_for_projects_req = sql_req.format(period_id) | ||
220 | + for( project_name, project_charge) in db.session.execute(charges_for_projects_req): | ||
221 | + project_charge = str(round(project_charge / charge_unit, 2)) if project_charge else '0' | ||
222 | + project_charges.append(project_charge) | ||
223 | + all_charges.append(project_charges) | ||
224 | + return all_charges | ||
225 | + | ||
226 | + | ||
205 | def charges_by_agent_stacked(agent_id): | 227 | def charges_by_agent_stacked(agent_id): |
206 | """ | 228 | """ |
207 | Build the list of charges for all projects of one agent, period by period | 229 | Build the list of charges for all projects of one agent, period by period |
app/main/routes.py
@@ -54,6 +54,13 @@ def projects(): | @@ -54,6 +54,13 @@ def projects(): | ||
54 | projects=all_projects) | 54 | projects=all_projects) |
55 | 55 | ||
56 | 56 | ||
57 | +@bp.route('/projects/stats') | ||
58 | +@role_required('project') | ||
59 | +def projects_stats(): | ||
60 | + num_projects = len(Project.query.all()) | ||
61 | + return render_template('projects_stats.html', subtitle="Statistiques des projets ({})".format(num_projects)) | ||
62 | + | ||
63 | + | ||
57 | @bp.route('/agents') | 64 | @bp.route('/agents') |
58 | @role_required('project') | 65 | @role_required('project') |
59 | def agents(): | 66 | def agents(): |
@@ -277,3 +284,16 @@ def charge_agent_csv(agent_id): | @@ -277,3 +284,16 @@ def charge_agent_csv(agent_id): | ||
277 | resp = make_response("\n".join(csv_table)) | 284 | resp = make_response("\n".join(csv_table)) |
278 | resp.headers['Content-Type'] = 'text/plain;charset=utf8' | 285 | resp.headers['Content-Type'] = 'text/plain;charset=utf8' |
279 | return resp | 286 | return resp |
287 | + | ||
288 | + | ||
289 | +@bp.route('/charge/projects') | ||
290 | +@role_required('project') | ||
291 | +def projects_stats_csv(): | ||
292 | + csv_table = [] | ||
293 | + for line in db_mgr.charges_for_projects_stacked(): | ||
294 | + line = [cell.replace(",", "-") for cell in line] | ||
295 | + line_string = ",".join(line) | ||
296 | + csv_table.append(line_string) | ||
297 | + resp = make_response("\n".join(csv_table)) | ||
298 | + resp.headers['Content-Type'] = 'text/plain;charset=utf8' | ||
299 | + return resp |
app/main/static/js/charges.js
@@ -7,7 +7,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -7,7 +7,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
7 | 7 | ||
8 | const main_elt = document.getElementById("main") | 8 | const main_elt = document.getElementById("main") |
9 | 9 | ||
10 | - const margin = {top: 60, right: 150, bottom: 200, left: 90}, | 10 | + var margin = {top: 60, right: 100, bottom: 200, left: 90}, |
11 | width = main_elt.offsetWidth * 0.95 - margin.left - margin.right, | 11 | width = main_elt.offsetWidth * 0.95 - margin.left - margin.right, |
12 | height = 600 - margin.top - margin.bottom; | 12 | height = 600 - margin.top - margin.bottom; |
13 | 13 | ||
@@ -18,32 +18,29 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -18,32 +18,29 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
18 | 18 | ||
19 | const y_ticks_num = 5 // dont show to many y ticks for lisibility | 19 | const y_ticks_num = 5 // dont show to many y ticks for lisibility |
20 | 20 | ||
21 | - const legendRectSize = 15; // size of rectangle in legend | ||
22 | - | ||
23 | - // const legend_cell_size = 15; // size of legend colored scare | ||
24 | - // const legend_x = width + 20; // begin legend a bit after the end of the chart | ||
25 | - // const legend_y = 600 - margin.bottom + 20; // begin legend a bit after the end of the chart | 21 | + const legend_rect_size = 15; // size of rectangle in legend |
22 | + const legend_height = 20; | ||
23 | + const legend_max_length = 20; | ||
24 | + const legend_spacing = 5; | ||
26 | 25 | ||
27 | const dot_radius = 6; // size of total line dot | 26 | const dot_radius = 6; // size of total line dot |
28 | 27 | ||
29 | - const xScale = d3.scaleBand() | ||
30 | - .range([0, width]) | ||
31 | - .padding(0.4); | ||
32 | - | ||
33 | - const yScale = d3.scaleLinear() | ||
34 | - .range([height, 0]); | ||
35 | 28 | ||
36 | var colorScale = d3.scaleOrdinal([]); // Will be really set later by category type | 29 | var colorScale = d3.scaleOrdinal([]); // Will be really set later by category type |
37 | 30 | ||
38 | - | ||
39 | // | 31 | // |
40 | // Configure chart by the category given as arg | 32 | // Configure chart by the category given as arg |
41 | // | 33 | // |
42 | var chart_title = "" | 34 | var chart_title = "" |
43 | var category_title = "" | 35 | var category_title = "" |
36 | + var draw_areas = () => void 0; | ||
44 | var draw_total_line = () => void 0; | 37 | var draw_total_line = () => void 0; |
45 | var draw_categories_bars = () => void 0; | 38 | var draw_categories_bars = () => void 0; |
46 | 39 | ||
40 | + var mouseoverlegend = category_mouseoverlegend; | ||
41 | + var mouseleavelegend = category_mouseleavelegend; | ||
42 | + | ||
43 | + | ||
47 | if (category_type == 'capacity') { | 44 | if (category_type == 'capacity') { |
48 | chart_title = "Charges par fonction" | 45 | chart_title = "Charges par fonction" |
49 | category_title = "Fonction" | 46 | category_title = "Fonction" |
@@ -61,10 +58,28 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -61,10 +58,28 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
61 | category_title = "Projet" | 58 | category_title = "Projet" |
62 | draw_categories_bars = draw_categories_stacked | 59 | draw_categories_bars = draw_categories_stacked |
63 | colorScale = d3.scaleOrdinal(d3.schemeTableau10); | 60 | colorScale = d3.scaleOrdinal(d3.schemeTableau10); |
61 | + } else if (category_type == 'area') { | ||
62 | + margin.bottom = 400 | ||
63 | + height = 1200 - margin.top - margin.bottom; | ||
64 | + chart_title = "Tous agents confondus" | ||
65 | + category_title = "Projet" | ||
66 | + draw_areas = draw_projects_areas | ||
67 | + colorScale = d3.scaleOrdinal(d3.schemeTableau10); | ||
68 | + mouseoverlegend = () => void 0; | ||
69 | + mouseleavelegend = () => void 0; | ||
64 | } else { | 70 | } else { |
65 | alert("ALERT ! Every body shall quit the boat, we are sinking ! ALERT !") | 71 | alert("ALERT ! Every body shall quit the boat, we are sinking ! ALERT !") |
66 | } | 72 | } |
67 | 73 | ||
74 | + | ||
75 | + const xScale = d3.scaleBand() | ||
76 | + .range([0, width]) | ||
77 | + .padding(0.4); | ||
78 | + | ||
79 | + const yScale = d3.scaleLinear() | ||
80 | + .range([height, 0]); | ||
81 | + | ||
82 | + | ||
68 | // Configure the tooltip to export svg to png or csv | 83 | // Configure the tooltip to export svg to png or csv |
69 | var update_export_menu = function (e, d) { | 84 | var update_export_menu = function (e, d) { |
70 | 85 | ||
@@ -134,6 +149,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -134,6 +149,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
134 | var category_name = d3.select(this.parentNode).datum().key | 149 | var category_name = d3.select(this.parentNode).datum().key |
135 | var category_charge = d.data[category_name] | 150 | var category_charge = d.data[category_name] |
136 | show_tooltip(e, category_name, category_charge) | 151 | show_tooltip(e, category_name, category_charge) |
152 | + console.log("HELLO") | ||
137 | } | 153 | } |
138 | 154 | ||
139 | var mouseovergrouped = function (e, d) { | 155 | var mouseovergrouped = function (e, d) { |
@@ -173,34 +189,34 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -173,34 +189,34 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
173 | .style("top", (e.pageY - tooltip_offset_dot.dy) + "px") | 189 | .style("top", (e.pageY - tooltip_offset_dot.dy) + "px") |
174 | } | 190 | } |
175 | 191 | ||
176 | - function mouseoverlegend() { | 192 | + function category_mouseoverlegend() { |
177 | var legend_category = $(this).attr("class"); | 193 | var legend_category = $(this).attr("class"); |
178 | var bar_category = document.getElementsByClassName(legend_category); | 194 | var bar_category = document.getElementsByClassName(legend_category); |
179 | if (bar_category[0].tagName === "g") { | 195 | if (bar_category[0].tagName === "g") { |
180 | var rect_to_hover = bar_category[0].children; | 196 | var rect_to_hover = bar_category[0].children; |
181 | (bar_category[1].children[0]).classList.add("brillance"); | 197 | (bar_category[1].children[0]).classList.add("brillance"); |
182 | - for (var i=0;i<rect_to_hover.length;i++) { | 198 | + for (var i = 0; i < rect_to_hover.length; i++) { |
183 | rect_to_hover[i].classList.add("brillance"); | 199 | rect_to_hover[i].classList.add("brillance"); |
184 | } | 200 | } |
185 | } else { | 201 | } else { |
186 | - for (var i=0;i<bar_category.length;i++) { | 202 | + for (var i = 0; i < bar_category.length; i++) { |
187 | bar_category[i].classList.add("brillance"); | 203 | bar_category[i].classList.add("brillance"); |
188 | } | 204 | } |
189 | } | 205 | } |
190 | } | 206 | } |
191 | 207 | ||
192 | - function mouseleavelegend() { | 208 | + function category_mouseleavelegend() { |
193 | var legend_category = $(this).attr("class"); | 209 | var legend_category = $(this).attr("class"); |
194 | var bar_category = document.getElementsByClassName(legend_category); | 210 | var bar_category = document.getElementsByClassName(legend_category); |
195 | if (bar_category[0].tagName === "g") { | 211 | if (bar_category[0].tagName === "g") { |
196 | var rect_to_hover = bar_category[0].children; | 212 | var rect_to_hover = bar_category[0].children; |
197 | (bar_category[1].children[0]).classList.remove("brillance"); | 213 | (bar_category[1].children[0]).classList.remove("brillance"); |
198 | - for (var i=0;i<rect_to_hover.length;i++) { | 214 | + for (var i = 0; i < rect_to_hover.length; i++) { |
199 | rect_to_hover[i].classList.remove("brillance"); | 215 | rect_to_hover[i].classList.remove("brillance"); |
200 | } | 216 | } |
201 | } else { | 217 | } else { |
202 | var lenght_i = bar_category.length - 1; | 218 | var lenght_i = bar_category.length - 1; |
203 | - for (var i=lenght_i;i>=0;i--) { | 219 | + for (var i = lenght_i; i >= 0; i--) { |
204 | bar_category[i].classList.remove("brillance"); | 220 | bar_category[i].classList.remove("brillance"); |
205 | } | 221 | } |
206 | } | 222 | } |
@@ -210,7 +226,13 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -210,7 +226,13 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
210 | 226 | ||
211 | // add horizontal legend | 227 | // add horizontal legend |
212 | let legend_keys = color_scale.domain(); | 228 | let legend_keys = color_scale.domain(); |
213 | - var legendSpacing = 5; | 229 | + |
230 | + // Truncate legend keys when too long | ||
231 | + legend_keys = legend_keys.map(function (l) { | ||
232 | + const n = legend_max_length; | ||
233 | + return (l.length > n) ? l.substr(0, n - 1) : l; | ||
234 | + }); | ||
235 | + | ||
214 | 236 | ||
215 | var legendWrap = svg.append('g') | 237 | var legendWrap = svg.append('g') |
216 | .attr('width', '100%') | 238 | .attr('width', '100%') |
@@ -220,38 +242,40 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -220,38 +242,40 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
220 | .data(legend_keys) | 242 | .data(legend_keys) |
221 | .enter() | 243 | .enter() |
222 | .append('g') | 244 | .append('g') |
223 | - .attr('class', function (d) { return d}) | 245 | + .attr('class', d => d) |
224 | .on("mouseover", mouseoverlegend) | 246 | .on("mouseover", mouseoverlegend) |
225 | .on("mouseleave", mouseleavelegend); | 247 | .on("mouseleave", mouseleavelegend); |
226 | 248 | ||
227 | legend.append('rect') | 249 | legend.append('rect') |
228 | - .attr('width', legendRectSize) | ||
229 | - .attr('height', legendRectSize) | 250 | + .attr('width', legend_rect_size) |
251 | + .attr('height', legend_rect_size) | ||
230 | .style('fill', color_scale) | 252 | .style('fill', color_scale) |
231 | .attr('class', 'legend'); | 253 | .attr('class', 'legend'); |
232 | 254 | ||
233 | legend.append('text') | 255 | legend.append('text') |
234 | .attr('class', 'legend') | 256 | .attr('class', 'legend') |
235 | - .attr('x', legendRectSize + legendSpacing * 1.5) | 257 | + .attr('x', legend_rect_size + legend_spacing * 1.5) |
236 | .attr('y', 13) | 258 | .attr('y', 13) |
237 | - .text(function (d) { | ||
238 | - return d; | ||
239 | - }); | 259 | + .text(d => d); |
240 | 260 | ||
241 | var ypos = 0, | 261 | var ypos = 0, |
242 | newxpos = 0, | 262 | newxpos = 0, |
243 | maxwidth = 0, | 263 | maxwidth = 0, |
244 | xpos; | 264 | xpos; |
245 | 265 | ||
266 | + // Get Max legend key length for later display | ||
267 | + const text_length_list = legend.selectAll('text.legend')._groups.map(el => el[0].getComputedTextLength()) | ||
268 | + const keycol_length = Math.max(...text_length_list) + 40 | ||
269 | + | ||
246 | legend | 270 | legend |
247 | .attr("transform", function (d, i) { | 271 | .attr("transform", function (d, i) { |
248 | - var length = d3.select(this).select("text").node().getComputedTextLength() + 40; | ||
249 | xpos = newxpos; | 272 | xpos = newxpos; |
250 | - if (width < xpos + length) { | 273 | + if (width < xpos + keycol_length) { |
251 | newxpos = xpos = 0; | 274 | newxpos = xpos = 0; |
252 | - ypos += 30; | 275 | + ypos += legend_height; |
276 | + | ||
253 | } | 277 | } |
254 | - newxpos += length; | 278 | + newxpos += keycol_length; |
255 | if (newxpos > maxwidth) maxwidth = newxpos; | 279 | if (newxpos > maxwidth) maxwidth = newxpos; |
256 | 280 | ||
257 | return 'translate(' + xpos + ',' + ypos + ')'; | 281 | return 'translate(' + xpos + ',' + ypos + ')'; |
@@ -263,11 +287,11 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -263,11 +287,11 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
263 | .attr("transform", function (d, i) { | 287 | .attr("transform", function (d, i) { |
264 | return "translate(0 ," + (height + 100) + ")"; | 288 | return "translate(0 ," + (height + 100) + ")"; |
265 | }); | 289 | }); |
266 | - | ||
267 | } | 290 | } |
268 | 291 | ||
269 | function draw_categories_grouped(data, categories) { | 292 | function draw_categories_grouped(data, categories) { |
270 | 293 | ||
294 | + // TODO: use ymax_from_stacked | ||
271 | // Get the max y to plot | 295 | // Get the max y to plot |
272 | // If no data at all, force to 0 | 296 | // If no data at all, force to 0 |
273 | if (categories.length == 0) { | 297 | if (categories.length == 0) { |
@@ -307,14 +331,16 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -307,14 +331,16 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
307 | .enter().append("rect") | 331 | .enter().append("rect") |
308 | .attr("x", d => xCategories(d.key)) | 332 | .attr("x", d => xCategories(d.key)) |
309 | .attr("width", xCategories.bandwidth()) | 333 | .attr("width", xCategories.bandwidth()) |
310 | - /* Transition part */ | ||
311 | - .attr("y", d => { return height; }) | ||
312 | - .attr("height", 0) | ||
313 | - .transition() | ||
314 | - .duration(750) | ||
315 | - .delay(function (d, i) { | ||
316 | - return i * 150; | ||
317 | - }) | 334 | + /* Transition part */ |
335 | + .attr("y", d => { | ||
336 | + return height; | ||
337 | + }) | ||
338 | + .attr("height", 0) | ||
339 | + .transition() | ||
340 | + .duration(750) | ||
341 | + .delay(function (d, i) { | ||
342 | + return i * 150; | ||
343 | + }) | ||
318 | .attr("y", d => yScale(d.value)) | 344 | .attr("y", d => yScale(d.value)) |
319 | .attr("height", d => height - yScale(d.value)) | 345 | .attr("height", d => height - yScale(d.value)) |
320 | .attr("fill", d => colorScale(d.key)) | 346 | .attr("fill", d => colorScale(d.key)) |
@@ -328,15 +354,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -328,15 +354,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
328 | 354 | ||
329 | } | 355 | } |
330 | 356 | ||
331 | - function draw_categories_stacked(data, categories) { | ||
332 | - | ||
333 | - // Now build the stacked data for stacked bars | ||
334 | - // | ||
335 | - var stack = d3.stack() | ||
336 | - .keys(categories) | ||
337 | - // .order(d3.stackOrderNone) | ||
338 | - // .offset(d3.stackOffsetNone); | ||
339 | - var stacked_data = stack(data) | 357 | + function ymax_from_stacked(stacked_data) { |
340 | 358 | ||
341 | // Get the max y to plot | 359 | // Get the max y to plot |
342 | // If no data at all, force to 0 | 360 | // If no data at all, force to 0 |
@@ -352,6 +370,52 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -352,6 +370,52 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
352 | // Enhance it by %ratio to leave room on the top of the graph | 370 | // Enhance it by %ratio to leave room on the top of the graph |
353 | y_max = y_max * height_ratio | 371 | y_max = y_max * height_ratio |
354 | 372 | ||
373 | + return y_max | ||
374 | + } | ||
375 | + | ||
376 | + function draw_projects_areas(data, projects) { | ||
377 | + | ||
378 | + // Now build the stacked data for stacked bars | ||
379 | + // | ||
380 | + var stack = d3.stack() | ||
381 | + .keys(projects) | ||
382 | + // .order(d3.stackOrderNone) | ||
383 | + // .offset(d3.stackOffsetNone); | ||
384 | + var stacked_data = stack(data) | ||
385 | + | ||
386 | + const y_max = ymax_from_stacked(stacked_data); | ||
387 | + yScale.domain([0, y_max]); | ||
388 | + | ||
389 | + const area = d3.area() | ||
390 | + .x(d => xScale(d.data.period)) | ||
391 | + .y0(d => yScale(d[0])) | ||
392 | + .y1(d => yScale(d[1])) | ||
393 | + | ||
394 | + let paths = svg.append("g") | ||
395 | + .selectAll("path") | ||
396 | + .data(stacked_data) | ||
397 | + .join("path") | ||
398 | + .attr("fill", ({key}) => colorScale(key)) | ||
399 | + .attr("stroke", 'black') | ||
400 | + .attr("stroke-width", '0.2pt') | ||
401 | + .attr("d", area) | ||
402 | + .append("title") | ||
403 | + .text(({key}) => key); | ||
404 | + | ||
405 | + } | ||
406 | + | ||
407 | + | ||
408 | + function draw_categories_stacked(data, categories) { | ||
409 | + | ||
410 | + // Now build the stacked data for stacked bars | ||
411 | + // | ||
412 | + var stack = d3.stack() | ||
413 | + .keys(categories) | ||
414 | + // .order(d3.stackOrderNone) | ||
415 | + // .offset(d3.stackOffsetNone); | ||
416 | + var stacked_data = stack(data) | ||
417 | + | ||
418 | + const y_max = ymax_from_stacked(stacked_data); | ||
355 | yScale.domain([0, y_max]); | 419 | yScale.domain([0, y_max]); |
356 | 420 | ||
357 | // first draw one group for one category containing all bars over periods | 421 | // first draw one group for one category containing all bars over periods |
@@ -370,14 +434,16 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -370,14 +434,16 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
370 | .attr("class", "bar") | 434 | .attr("class", "bar") |
371 | .attr("x", d => xScale(d.data.period)) | 435 | .attr("x", d => xScale(d.data.period)) |
372 | .attr("width", xScale.bandwidth()) | 436 | .attr("width", xScale.bandwidth()) |
373 | - /* transition part */ | ||
374 | - .attr("y", d => { return height; }) | ||
375 | - .attr("height", 0) | ||
376 | - .transition() | ||
377 | - .duration(750) | ||
378 | - .delay(function (d, i) { | ||
379 | - return i * 150; | ||
380 | - }) | 437 | + /* transition part */ |
438 | + .attr("y", d => { | ||
439 | + return height; | ||
440 | + }) | ||
441 | + .attr("height", 0) | ||
442 | + .transition() | ||
443 | + .duration(750) | ||
444 | + .delay(function (d, i) { | ||
445 | + return i * 150; | ||
446 | + }) | ||
381 | .attr("y", d => yScale(d[1])) | 447 | .attr("y", d => yScale(d[1])) |
382 | .attr("height", d => height - yScale(d[1] - d[0])) | 448 | .attr("height", d => height - yScale(d[1] - d[0])) |
383 | 449 | ||
@@ -437,8 +503,8 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -437,8 +503,8 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
437 | 503 | ||
438 | d3.csv(data_url).then(data => { | 504 | d3.csv(data_url).then(data => { |
439 | // we could get categories that way, | 505 | // we could get categories that way, |
440 | - // but instead we want to filter by non zero, see later | ||
441 | // var categories = data.columns.slice(1) | 506 | // var categories = data.columns.slice(1) |
507 | + // but instead we want to filter by non zero, see later | ||
442 | 508 | ||
443 | var periods = d3.map(data, d => d.period) | 509 | var periods = d3.map(data, d => d.period) |
444 | xScale.domain(periods) | 510 | xScale.domain(periods) |
@@ -477,6 +543,11 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | @@ -477,6 +543,11 @@ function build_chart(div_selector, data_url, entity_name, category_type) { | ||
477 | } | 543 | } |
478 | 544 | ||
479 | // | 545 | // |
546 | + // Draw the areas, stacked. | ||
547 | + // | ||
548 | + draw_areas(data, categories) | ||
549 | + | ||
550 | + // | ||
480 | // Draw the bars, stacked or grouped | 551 | // Draw the bars, stacked or grouped |
481 | // | 552 | // |
482 | draw_categories_bars(data, categories) | 553 | draw_categories_bars(data, categories) |
@@ -0,0 +1,25 @@ | @@ -0,0 +1,25 @@ | ||
1 | +{% extends "base_page.html" %} | ||
2 | +{% block more_heads %} | ||
3 | + <link href="{{ url_for('main.static', filename='css/charges.css', version=config.VERSION) }}" rel="stylesheet" | ||
4 | + type="text/css"/> | ||
5 | +{% endblock %} | ||
6 | + | ||
7 | +{% block content %} | ||
8 | + | ||
9 | + <!-- Invisible span to definte wich ul and a in the navbar are actived --> | ||
10 | + <span id="nav_actived" style="display: none">project,projects_stats</span> | ||
11 | + | ||
12 | + <div class="charge_chart" id="projects_stats_chart"></div> | ||
13 | +{% endblock %} | ||
14 | + | ||
15 | +{% block more_scripts %} | ||
16 | + {% include 'd3js-includes.html' %} | ||
17 | + {% include 'charges-includes.html' %} | ||
18 | + | ||
19 | + <script> | ||
20 | + build_chart("#projects_stats_chart", | ||
21 | + "{{url_for('main.projects_stats_csv')}}", | ||
22 | + "Charge des projets", | ||
23 | + "area"); | ||
24 | + </script> | ||
25 | +{% endblock %} |
app/templates/base_page.html
@@ -71,7 +71,7 @@ | @@ -71,7 +71,7 @@ | ||
71 | <li class="nav-item"><a id="categories" class="sub_link nav-link" href="{{ url_for('main.categories') }}">Liste des catégories</a></li> | 71 | <li class="nav-item"><a id="categories" class="sub_link nav-link" href="{{ url_for('main.categories') }}">Liste des catégories</a></li> |
72 | <li class="nav-item"><a id="labels" class="sub_link nav-link" href="{{ url_for('main.labels') }}">Liste des labels</a></li> | 72 | <li class="nav-item"><a id="labels" class="sub_link nav-link" href="{{ url_for('main.labels') }}">Liste des labels</a></li> |
73 | <li class="nav-item"><a class="sub_link nav-link disabled" href="#">Listes des statuts de projets</a> | 73 | <li class="nav-item"><a class="sub_link nav-link disabled" href="#">Listes des statuts de projets</a> |
74 | - <li class="nav-item"><a class="sub_link nav-link disabled" href="#">Statistiques</a></li> | 74 | + <li class="nav-item"><a id="projects_stats" class="sub_link nav-link" href="{{ url_for('main.projects_stats') }}">Statistiques</a></li> |
75 | </li> | 75 | </li> |
76 | </ul> | 76 | </ul> |
77 | </li> | 77 | </li> |
tests/backend_tests.py
@@ -75,6 +75,12 @@ class DbMgrTestCase(BaseTestCase): | @@ -75,6 +75,12 @@ class DbMgrTestCase(BaseTestCase): | ||
75 | # Waiting for 17 periods + headers line | 75 | # Waiting for 17 periods + headers line |
76 | self.assertEqual(18, len(stacked_charges)) | 76 | self.assertEqual(18, len(stacked_charges)) |
77 | 77 | ||
78 | + def test_charges_for_projects_stacked(self): | ||
79 | + stacked_charges = db_mgr.charges_for_projects_stacked() | ||
80 | + # Waiting for 17 periods + headers line | ||
81 | + self.assertEqual(18, len(stacked_charges)) | ||
82 | + self.assertEqual(102, len(stacked_charges[0])) | ||
83 | + | ||
78 | def test_category_labels(self): | 84 | def test_category_labels(self): |
79 | category_labels = db_mgr.category_labels() | 85 | category_labels = db_mgr.category_labels() |
80 | categories = [_cl['name'] for _cl in category_labels] | 86 | categories = [_cl['name'] for _cl in category_labels] |