Index: lams_central/web/includes/javascript/chart.js =================================================================== diff -u -ra83b0eec89979dce7415b02afdda324b14018dbb -r5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe --- lams_central/web/includes/javascript/chart.js (.../chart.js) (revision a83b0eec89979dce7415b02afdda324b14018dbb) +++ lams_central/web/includes/javascript/chart.js (.../chart.js) (revision 5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe) @@ -1,65 +1,76 @@ /** - * Prepares SVG area for drawing and runs concrete chart function. + * Identifies data source and runs chart drawing function. */ -function drawChart(type, chartID, url, legendOnHover){ - // get data with the given URL - d3.json(url, function(error, response){ - if (error) { - // forward error to browser - throw error; - } - - if (!response || $.isEmptyObject(response)) { - // if there is no data to display - return; - } - - // clear previous chart - var rawData = response.data, - chartDiv = $('#' + chartID).empty().show(), - width = chartDiv.width(), - height = chartDiv.height(), - // add SVG elem - svg = d3.select(chartDiv[0]) - .append('svg') - .attr('width', width) - .attr('height', height); - // build domain out of keys - domainX = rawData.map(function(d) { return d.name }), - // 10 color palette - scaleColor = d3.scaleOrdinal(d3.schemeCategory10) - .domain(domainX), - legend = null; - - if (!legendOnHover) { - legend = svg.append('g'); - // build legend first so we know how much space we've got for the chart - $.each(rawData, function(index, d){ - // a small rectangle with proper color - legend.append('rect') - .attr('x', 0) - .attr('y', index * 30) - .attr('width', 15) - .attr('height', 15) - .attr('fill', scaleColor(d.name)); - // label - legend.append('text') - .attr('x', 20) - .attr('y', index * 30 + 11) - .attr('text-anchor', 'start') - .text(d.name + ' (' + Math.round(+d.value) + ' %)'); +function drawChart(type, chartID, dataSource, legendOnHover){ + switch (typeof dataSource) { + case 'string': + // get data with the given URL + d3.json(dataSource, function(error, response){ + if (error) { + // forward error to browser + throw error; + } + + if (!response || $.isEmptyObject(response)) { + // if there is no data to display + return; + } + + _drawChart(type, chartID, response.data, legendOnHover); }); - } - - // draw proper chart - if (type == 'bar') { - _drawBarChart(svg, legend, width, height, rawData, domainX, scaleColor); - } else if (type == 'pie') { - _drawPieChart(svg, legend, width, height, rawData, scaleColor); - } - }); + break; + case 'object': + _drawChart(type, chartID, dataSource, legendOnHover); + break; + } +} +/** + * Prepares SVG area for drawing and runs concrete chart function. + */ +function _drawChart(type, chartID, rawData, legendOnHover) { + // clear previous chart + var chartDiv = $('#' + chartID).empty().show(), + width = chartDiv.width(), + height = chartDiv.height(), + // add SVG elem + svg = d3.select(chartDiv[0]) + .append('svg') + .attr('width', width) + .attr('height', height); + // build domain out of keys + domainX = rawData.map(function(d) { return d.name }), + // 10 color palette + scaleColor = d3.scaleOrdinal(d3.schemeCategory10) + .domain(domainX), + legend = null; + + if (!legendOnHover) { + legend = svg.append('g'); + // build legend first so we know how much space we've got for the chart + $.each(rawData, function(index, d){ + // a small rectangle with proper color + legend.append('rect') + .attr('x', 0) + .attr('y', index * 30) + .attr('width', 15) + .attr('height', 15) + .attr('fill', scaleColor(d.name)); + // label + legend.append('text') + .attr('x', 20) + .attr('y', index * 30 + 11) + .attr('text-anchor', 'start') + .text(d.name + ' (' + Math.round(+d.value) + '%)'); + }); + } + // draw proper chart + if (type == 'bar') { + _drawBarChart(chartDiv, legend, width, height, rawData, domainX, scaleColor); + } else if (type == 'pie') { + _drawPieChart(chartDiv, legend, width, height, rawData, scaleColor); + } } // margins are needed so bar chart Y axis is not clipped @@ -69,10 +80,11 @@ /** * Draws a bar chart. */ -function _drawBarChart(svg, legend, width, height, rawData, domainX, scaleColor) { +function _drawBarChart(chartDiv, legend, width, height, rawData, domainX, scaleColor) { // if all bars easily fit in a half of SVG width, limit the drawing width // otherwise the chart would be too wide - var tooltip = legend ? null : d3.select($(svg.node()).parent()[0]) + var svg = d3.select(chartDiv[0]).select('svg'), + tooltip = legend ? null : d3.select(chartDiv[0]) .append('div') .attr('class', 'chartTooltip'), legendWidth = legend ? legend.node().getBBox().width : 0, @@ -123,7 +135,7 @@ tooltip.transition() .duration(200) .style('opacity', 1); - tooltip.text(d.name + ' (' + Math.round(+d.value) + ' %)') + tooltip.text(d.name + ' (' + Math.round(+d.value) + '%)') .style('left', +offset.left + box.width/2 - $(tooltip.node()).width()/2 - 22 + 'px') .style('top', +offset.top - 20 + 'px'); } @@ -145,9 +157,10 @@ /** * Draws a pie chart. */ -function _drawPieChart(svg, legend, width, height, rawData, scaleColor){ +function _drawPieChart(chartDiv, legend, width, height, rawData, scaleColor){ // calculate how much space we've got for the chart - var tooltip = legend ? null : d3.select($(svg.node()).parent()[0]) + var svg = d3.select(chartDiv[0]).select('svg'), + tooltip = legend ? null : d3.select(chartDiv[0]) .append('div') .attr('class', 'chartTooltip'), legendWidth = legend ? legend.node().getBBox().width : 0, @@ -182,7 +195,7 @@ tooltip.transition() .duration(200) .style('opacity', 1); - tooltip.text(d.data.name + ' (' + Math.round(+d.data.value) + ' %)') + tooltip.text(d.data.name + ' (' + Math.round(+d.data.value) + '%)') .style('left', +offset.left + box.width/2 + 'px') .style('top', +offset.top + box.height/2 + 'px'); } @@ -199,6 +212,21 @@ // move the legend to the right of the chart legend.attr('transform', 'translate(' + (radius * 2 + CHART_MARGIN.right) + ',' + (CHART_MARGIN.top + 20) + ')'); } + + // function to animate changed data + canvas.selectAll("path").each(function(d){ + this.currentData = d; + }); + chartDiv.data('updateFunctions', { + 'pie' : pie, + 'arcTween' : function(a) { + var interpolation = d3.interpolate(this.currentData, a); + this.currentData = interpolation(0); + return function(t) { + return arc(interpolation(t)); + }; + } + }); } function drawHistogram(chartID, url, xAxisLabel, yAxisLabel){ @@ -427,5 +455,4 @@ return focusbar; } -} - +} \ No newline at end of file Index: lams_learning/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -rc32fc3365594b9d572ea07094f580218cf952c93 -r5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe --- lams_learning/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision c32fc3365594b9d572ea07094f580218cf952c93) +++ lams_learning/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe) @@ -171,7 +171,7 @@ label.kumalive.poll.answer.custom =Custom... label.kumalive.poll.answer.custom.tip ={Put} {each} {answer} {in} {curly} {brackets} label.kumalive.poll.answer.custom.error.syntax =Custom answer syntax is incorrect. Example: {first answer} {second answer} -label.kumalive.poll.answer.custom.error.count =Maximum 10 answers allowed +label.kumalive.poll.answer.custom.error.count =Maximum 9 answers allowed button.kumalive.poll.start =Ask now! button.kumalive.poll.vote =Vote button.kumalive.poll.finish =Finish poll @@ -181,5 +181,7 @@ message.kumalive.poll.release.votes.confirm =Are you sure you want to show poll results to students who voted? button.kumalive.poll.release.voters =Show votes & voters to learners message.kumalive.poll.release.voters.confirm =Are you sure you want to show poll results and voters' names to students? - +label.kumalive.poll.missing.voters =Not voted +label.kumalive.poll.results =Results +label.kumalive.poll.votes.total =Total votes #======= End labels: Exported 151 labels for en AU ===== Index: lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java =================================================================== diff -u -rc32fc3365594b9d572ea07094f580218cf952c93 -r5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe --- lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java (.../KumaliveWebsocketServer.java) (revision c32fc3365594b9d572ea07094f580218cf952c93) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/kumalive/KumaliveWebsocketServer.java (.../KumaliveWebsocketServer.java) (revision 5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe) @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -96,6 +97,7 @@ private final JSONObject pollJSON = new JSONObject(); private final List answerIds = new ArrayList(); private final ArrayList> voters = new ArrayList>(); + private final Set voterLogins = ConcurrentHashMap.newKeySet(); private final JSONArray votersJSON = new JSONArray(); private boolean finished = false; private boolean votesReleased = false; @@ -384,13 +386,27 @@ votesJSON.put(voters.size()); JSONArray answerVotersJSON = kumalive.poll.votersJSON.getJSONArray(answerIndex); for (int voterIndex = answerVotersJSON.length(); voterIndex < voters.size(); voterIndex++) { - answerVotersJSON.put(KumaliveWebsocketServer.participantToJSON(voters.get(voterIndex), null)); + UserDTO voter = voters.get(voterIndex); + // add voter to "all voters" poll so we can calculate missing voters later + kumalive.poll.voterLogins.add(voter.getLogin()); + answerVotersJSON.put(KumaliveWebsocketServer.participantToJSON(voter, null)); } } } pollJSON.put("votes", votesJSON); pollJSON.put("voters", kumalive.poll.votersJSON); + + // calculate missing voters + JSONArray missingVotersJSON = new JSONArray(); + for (Entry learnerEntry : kumalive.learners.entrySet()) { + if (!learnerEntry.getValue().roleTeacher + && !kumalive.poll.voterLogins.contains(learnerEntry.getKey())) { + missingVotersJSON.put(learnerEntry.getValue().userDTO.getUserID()); + } + } + pollJSON.put("missingVotes", missingVotersJSON.length()); + pollJSON.put("missingVoters", missingVotersJSON); } String learnerResponse = responseJSON.toString(); @@ -413,9 +429,11 @@ // put them in response only if teacher released them and user voted if (!kumalive.poll.votesReleased || (!voted && !kumalive.poll.finished)) { learnerPollJSON.remove("votes"); + learnerPollJSON.remove("missingVotes"); } if (!kumalive.poll.votersReleased || (!voted && !kumalive.poll.finished)) { learnerPollJSON.remove("voters"); + learnerPollJSON.remove("missingVoters"); } if (voted) { // mark this learner as voted @@ -837,6 +855,7 @@ userDTO = user.getUserDTO(); } answerVoters.add(userDTO); + pollDTO.voterLogins.add(userDTO.getLogin()); answerVotersJSON.put(KumaliveWebsocketServer.participantToJSON(userDTO, null)); } } Index: lams_learning/web/css/kumalive.scss =================================================================== diff -u -rc32fc3365594b9d572ea07094f580218cf952c93 -r5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe --- lams_learning/web/css/kumalive.scss (.../kumalive.scss) (revision c32fc3365594b9d572ea07094f580218cf952c93) +++ lams_learning/web/css/kumalive.scss (.../kumalive.scss) (revision 5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe) @@ -55,10 +55,15 @@ width: initial; } -#pollSetup, #pollSetupAnswerCustom, #pollSetupAnswerCustomParseError, #pollSetupAnswerCustomCountError { +#pollSetupAnswerCustom, #pollSetupAnswerCustomParseError, #pollSetupAnswerCustomCountError { display: none; } +#pollSetup { + display: none; + padding: 0 10px; +} + #pollSetupButtons, #pollSetup h3 { text-align: center; } @@ -99,14 +104,29 @@ text-align: left; } +#pollRunAnswerList .list-group-item.voted { + border: $border-thin-primary; + border-width: thick; +} + #pollRunAnswerList .list-group-item .badge { margin-left: 5px; } +#pollRunChart { + display: none; + border-top: $border-thin-black; + margin-top: 10px; +} + +#pollRunChartPie { + height: 320px; +} + #pollRun .pollVoters { margin-top: 10px; text-align: left; - border-top: $border-thin-black; + border-top: $border-thin-default-light; } #pollRun .pollVoters .badge { @@ -119,6 +139,10 @@ padding: 0; } +#learnersCell h3 { + text-align: center; +} + #learnersContainer { padding-left: 10px; } @@ -238,23 +262,21 @@ #actionCell { width: 350px; height: 100vh; - margin-right: 10px; border-bottom: none; border-right: $border-thin-black; } #pollCell { width: 500px; height: 100vh; - padding-left: 0; - margin-right: 10px; + padding: 0; border-bottom: none; border-right: $border-thin-black; } .learner { height: 120px; - margin-right: 20px; + margin-left: 20px; } .profilePicture { Index: lams_learning/web/includes/javascript/kumalive.js =================================================================== diff -u -rc32fc3365594b9d572ea07094f580218cf952c93 -r5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe --- lams_learning/web/includes/javascript/kumalive.js (.../kumalive.js) (revision c32fc3365594b9d572ea07094f580218cf952c93) +++ lams_learning/web/includes/javascript/kumalive.js (.../kumalive.js) (revision 5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe) @@ -346,43 +346,110 @@ // open panel if closed $('#actionCell .pollButton').prop('disabled', true); - $('#pollCell, #pollRun').show(); + $('#pollCell').show(); + var pollRunDiv = $('#pollRun').show(); // init poll fields or make them read only after voting - if (poll.id != pollId || (poll.finished && $('#pollRunVoteButton').is(':visible'))) { + if (poll.id != pollId || (poll.finished && $('#pollRunVoteButton', pollRunDiv).is(':visible'))) { initPoll(poll); } if (poll.voted != null) { // highlight the answer user voted for - $('#pollAnswer' + poll.voted).addClass('active'); + $('#pollAnswer' + poll.voted, pollRunDiv).addClass('voted'); } + + // update counters and charts if (poll.votes) { + $('#pollRunChart', pollRunDiv).show(); + + var chartData = [], + voterCount = 0; // show votes if user is teacher or votes were released - $.each(poll.votes, function(index, count) { - var answerElement = $('#pollAnswer' + index), + $.each(poll.votes, function(answerIndex, count) { + var answerElement = $('#pollAnswer' + answerIndex, pollRunDiv), badge = $('.badge', answerElement); // missing badge means that votes were made available just now if (badge.length === 0) { - badge = $('').addClass('badge').appendTo(answerElement); + // its colour corresponds to chart + badge = $('').addClass('badge').css('background-color', d3.schemeCategory10[answerIndex]) + .appendTo(answerElement); } + // update visual counter badge.text(count); + + // build data to feed chart + chartData.push({ + 'name' : pollAnswerBullets[answerIndex], + 'value': count + }); + + // count all voters, no matter what they chose + voterCount += count; }); + + // rewrite number of voters into percent + var learnerCount = voterCount + poll.missingVotes, + chartDiv = $('#pollRunChartPie', pollRunDiv); + $('#pollRunTotalVotes').text(voterCount + '/' + learnerCount + ' (' + + Math.round(voterCount / learnerCount * 100) + '%)'); + $.each(chartData, function() { + this.value = Math.round(this.value / learnerCount * 100); + }); + // add missing voters + chartData.push({ + 'name' : LABELS.MISSING_VOTERS, + 'value': Math.round(poll.missingVotes / learnerCount * 100) + }); + + if (chartDiv.is(':empty')) { + // draw a new chart + drawChart('pie', 'pollRunChartPie', chartData, false); + } else { + // update chart data using functions set in chart.js + var updateFunctions = chartDiv.data('updateFunctions'); + d3.select(chartDiv[0]).selectAll('path').data(updateFunctions.pie(chartData)) + .transition().duration(750).attrTween("d", updateFunctions.arcTween); + // update legend + chartDiv.find('text').each(function(answerIndex, legendItem){ + $(legendItem).text(chartData[answerIndex].name + ' (' + chartData[answerIndex].value + '%)'); + }); + } } + + // update voter icons and counters if (poll.voters) { - $.each(poll.voters, function(answerIndex, answerVoters) { - var answerVotersContainer = $('#pollVoters' + answerIndex); - if (answerVotersContainer.length === 0) { + // no voters yet, i.e. page refreshed or voters just released + if ($('.pollVoters', pollRunDiv).length === 0){ + $.each(poll.voters, function(answerIndex, answerVoters) { + // build a container for each answer + var answerVotersContainer = $('#pollVoters' + answerIndex, pollRunDiv); answerVotersContainer = $('
').attr('id', 'pollVoters' + answerIndex).addClass('pollVoters') - .appendTo('#pollRun'); - $('').addClass('badge').appendTo(answerVotersContainer); - $('

').text(pollAnswerBullets[answerIndex] + ') ' + poll.answers[answerIndex]).appendTo(answerVotersContainer); - } + .appendTo(pollRunDiv); + $('').addClass('badge').css('background-color', d3.schemeCategory10[answerIndex]) + .appendTo(answerVotersContainer); + $('

').text(pollAnswerBullets[answerIndex] + ') ' + poll.answers[answerIndex]) + .appendTo(answerVotersContainer); + }); + // build a container for missing voters + var missingVotersContainer = $('
').attr('id', 'pollVotersMissing').addClass('pollVoters').appendTo(pollRunDiv); + $('').addClass('badge').css('background-color', d3.schemeCategory10[poll.voters.length]) + .appendTo(missingVotersContainer); + $('

').text("Not voted").appendTo(missingVotersContainer); + } + + // fill each voter container with voters + var learnerDivs = $('#learnersContainer .learner'); + $.each(poll.voters, function(answerIndex, answerVoters) { + // update counter + var answerVotersContainer = $('#pollVoters' + answerIndex, pollRunDiv); $('.badge', answerVotersContainer).text(poll.votes[answerIndex]); $.each(answerVoters, function(voterIndex, voter) { + // if a voter is already added, skip if ($('.learner[userId="' + voter.id + '"]', answerVotersContainer).length !== 0) { return true; } + // create a voter icon var voterDiv = learnerDivTemplate.clone() .attr('userId', voter.id) .appendTo(answerVotersContainer), @@ -397,12 +464,37 @@ } learnerFadeIn(voterDiv); - var learnerDiv = $('#learnersContainer .learner[userId="' + voter.id + '"]'); + // add bagde to user in Learners section + var learnerDiv = learnerDivs.filter('[userId="' + voter.id + '"]'); if ( $('.badge', learnerDiv).length === 0) { - $('').addClass('badge').text(pollAnswerBullets[answerIndex]).prependTo(learnerDiv); + $('').addClass('badge').css('background-color', d3.schemeCategory10[answerIndex]) + .text(pollAnswerBullets[answerIndex]).prependTo(learnerDiv); } }); }); + + // fill missing voters container + var missingVotersContainer = $('#pollVotersMissing'), + missingVoters = $('.learner', missingVotersContainer); + + $('.badge', missingVotersContainer).text(poll.missingVotes); + + // remove missing voters because they voted or logged out + missingVoters.filter(function(){ + return poll.missingVoters.indexOf(+$(this).attr('userId')) === -1; + }).each(function(){ + learnerFadeOut($(this)); + }); + + // add missing voters + $.each(poll.missingVoters, function(){ + if (missingVoters.index('.learner[userId="' + this + '"]') === -1) { + var learnerDiv = learnerDivs.filter('.learner[userId="' + this + '"]'), + voterDiv = learnerDiv.clone().removeClass('changing').css('cursor', 'default').appendTo(missingVotersContainer); + $('.badge', voterDiv).remove(); + $('.profilePicture', voterDiv).removeClass('profilePictureHidden').css('opacity', ''); + } + }); } } @@ -749,10 +841,6 @@ var answerElement = $('
  • ').addClass('list-group-item').attr('id', 'pollAnswer' + index) .text(pollAnswerBullets[index] + ') ' + answer) .appendTo(answerList); - if (poll.votes) { - // show votes if user is teacher or votes were released - $('').addClass('badge').text(poll.votes[index]).prependTo(answerElement); - } }); $('#pollRunAnswerList').show(); // extra options for teacher @@ -841,7 +929,7 @@ if (answers.length === 0) { $('#pollSetupAnswerCustomGroup').addClass('has-error'); $('#pollSetupAnswerCustomParseError').show(); - } else if (answers.length > 10) { + } else if (answers.length > 9) { $('#pollSetupAnswerCustomGroup').addClass('has-error'); $('#pollSetupAnswerCustomCountError').show(); } else { Index: lams_learning/web/kumalive/kumalive.jsp =================================================================== diff -u -rc32fc3365594b9d572ea07094f580218cf952c93 -r5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe --- lams_learning/web/kumalive/kumalive.jsp (.../kumalive.jsp) (revision c32fc3365594b9d572ea07094f580218cf952c93) +++ lams_learning/web/kumalive/kumalive.jsp (.../kumalive.jsp) (revision 5b76fc85b49c6dc9dbedfac8b235e9e3d56ef8fe) @@ -16,6 +16,8 @@ + + @@ -86,7 +90,7 @@ -
    +
  • @@ -173,14 +177,20 @@ +
    +

    +

     

    +
    +
    +

    -

    +

    -

    +