Merge branch 'module/poll' into dev
This commit is contained in:
@@ -144,6 +144,7 @@ public class Field {
|
||||
public static final String START_DATE = "start_date";
|
||||
public static final String START_TIME = "start_time";
|
||||
public static final String STATE = "state";
|
||||
public static final String STATS = "stats";
|
||||
public static final String STATUS = "status";
|
||||
public static final String STATUS_CODE = "code";
|
||||
public static final String SUBJECT = "subject";
|
||||
|
||||
@@ -107,18 +107,79 @@ public class Poll implements Mappable {
|
||||
|
||||
}
|
||||
public static class Evaluation {
|
||||
// Map<Option → Map<Weight → Count>>
|
||||
private HashMap<Integer,Map<Integer,Integer>> selections = new HashMap<>();
|
||||
public void count(ResultSet rs) throws SQLException {
|
||||
var optionId = rs.getInt(OPTION_ID);
|
||||
var userId = rs.getObject(USER);
|
||||
var weight = rs.getInt(WEIGHT);
|
||||
var optionStats = selections.computeIfAbsent(optionId, k -> new HashMap<>());
|
||||
optionStats.compute(weight, (k, sum) -> sum == null ? 1 : sum + 1);
|
||||
private static class Histogram extends HashMap<Integer,Integer>{
|
||||
public Double average() {
|
||||
var sum = 0;
|
||||
var count = 0;
|
||||
for (var entry : entrySet()){
|
||||
var weight = entry.getKey();
|
||||
var votes = entry.getValue();
|
||||
count += votes;
|
||||
sum += weight * votes;
|
||||
}
|
||||
if (count < 1) return null;
|
||||
return sum/(double) count;
|
||||
}
|
||||
}
|
||||
private static class OptionStats extends HashMap<Integer,Histogram>{}
|
||||
private static class Stats extends HashMap<Double,OptionStats>{
|
||||
public Stats(OptionStats optionStats) {
|
||||
for (var entry : optionStats.entrySet()){
|
||||
var optionId = entry.getKey();
|
||||
var histo = entry.getValue();
|
||||
var average = histo.average();
|
||||
var os = get(average);
|
||||
if (os == null) put(average,os = new OptionStats());
|
||||
os.put(optionId,histo);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
{
|
||||
options : {
|
||||
1: option 1
|
||||
2: option 2
|
||||
},
|
||||
stat : {
|
||||
0.571 : { // average
|
||||
1 : { // option
|
||||
-2 : 0, // weight → selections
|
||||
-1 : 1,
|
||||
0 : 2
|
||||
1 : 3
|
||||
2 : 1
|
||||
}
|
||||
2 :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HashMap<Integer, Map<Integer, Integer>> toMap() {
|
||||
return selections;
|
||||
*/
|
||||
|
||||
private final Map<Integer, Option> options = new HashMap<>();
|
||||
private final OptionStats optionStats = new OptionStats();
|
||||
|
||||
public Evaluation(Poll poll) {
|
||||
for (var option : poll.options){
|
||||
options.put(option.id,option);
|
||||
var histo = new Histogram();
|
||||
for (var w : poll.weights.keySet()) histo.put(w,0);
|
||||
optionStats.put(option.id,histo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void count(ResultSet rs) throws SQLException {
|
||||
var optionId = rs.getInt(OPTION_ID);
|
||||
//var userId = rs.getObject(USER);
|
||||
var weight = rs.getInt(WEIGHT);
|
||||
var histogram = optionStats.get(optionId);
|
||||
histogram.put(weight,histogram.get(weight)+1);
|
||||
}
|
||||
|
||||
public Map<Double,?> toMap() {
|
||||
return new Stats(optionStats);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
28
frontend/src/Components/Histogram.svelte
Normal file
28
frontend/src/Components/Histogram.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import { t } from '../translations.svelte';
|
||||
let { data = {} } = $props();
|
||||
|
||||
let max = $derived(Math.max(...Object.values(data)));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.histo {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex; /* flexbox for text alignment */
|
||||
align-items: flex-end; /* text sticks to bottom of bar */
|
||||
height: 100%; /* full height of flex item */
|
||||
padding: 0 2px; /* space for text */
|
||||
box-sizing: border-box; /* include padding in height */
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="histo">
|
||||
{#each Object.entries(data).sort((a,b) => a[0] - b[0]) as [weight,count]}
|
||||
<div class="bar" style="height: {100*count/max}%" title={t('voted {count} times',{count})}>{weight}</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import Histogram from '../../Components/Histogram.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { api, get } from '../../urls.svelte';
|
||||
import { error, yikes } from '../../warn.svelte';
|
||||
@@ -62,25 +63,22 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(poll.evaluation) as [option_id,hist]}
|
||||
{#each Object.entries(poll.evaluation).sort((a,b) => b[0] - a[0]) as [avg,optionset]}
|
||||
{#each Object.entries(optionset) as [option_id,histo]}
|
||||
<tr>
|
||||
<td>
|
||||
{poll.options[option_id].name}
|
||||
</td>
|
||||
<td>
|
||||
{average(hist)}
|
||||
{(+avg).toFixed(3)}
|
||||
</td>
|
||||
<td class="histogram">
|
||||
{#each Object.entries(hist).sort((a,b) => a[0] - b[0]) as [weight,count]}
|
||||
<span style="height: {100*count/max_val(hist)}%" title={t('voted {count} times',{count:count})}>
|
||||
<span>{weight}</span>
|
||||
</span>
|
||||
{/each}
|
||||
<td>
|
||||
<Histogram data={histo} />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
{/if}
|
||||
<span class="warn">TODO: sort by average</span>
|
||||
@@ -10,7 +10,7 @@ import java.util.Map;
|
||||
public interface PollDb {
|
||||
Collection<Poll> listPolls(UmbrellaUser user);
|
||||
|
||||
Poll.Evaluation loadEvaluation(String id);
|
||||
Poll.Evaluation loadEvaluation(Poll poll);
|
||||
|
||||
Poll loadPoll(String id);
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ public class PollModule extends BaseHandler implements PollService {
|
||||
}
|
||||
}
|
||||
var result = new HashMap<>(poll.toMap());
|
||||
var evaluation = pollDb.loadEvaluation(poll.id());
|
||||
var evaluation = pollDb.loadEvaluation(poll);
|
||||
result.put(Field.EVALUATION,evaluation.toMap());
|
||||
return sendContent(ex,result);
|
||||
}
|
||||
|
||||
@@ -132,25 +132,27 @@ public class SqliteDb extends BaseDb implements PollDb {
|
||||
ps.setLong(1,user.id());
|
||||
ps.setLong(2, user.id());
|
||||
var rs = ps.executeQuery();
|
||||
var list = new ArrayList<Poll>();
|
||||
var map = new HashMap<String,Poll>();
|
||||
while (rs.next()) {
|
||||
var pollId = rs.getString(ID);
|
||||
if (map.containsKey(pollId)) continue;
|
||||
var poll = Poll.of(rs);
|
||||
var perm = rs.getInt(PERMISSION);
|
||||
if (perm != 0) poll.permissions().put(user,Permission.of(perm));
|
||||
list.add(poll);
|
||||
map.put(pollId,poll);
|
||||
}
|
||||
rs.close();
|
||||
return list;
|
||||
return map.values().stream().sorted(Comparator.comparing(Poll::name)).toList();
|
||||
} catch (SQLException sqle){
|
||||
throw failedToLoadObject(TABLE_POLLS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Poll.Evaluation loadEvaluation(String pollId) {
|
||||
public Poll.Evaluation loadEvaluation(Poll poll) {
|
||||
try {
|
||||
var result = new Poll.Evaluation();
|
||||
var rs = select(ALL).from(TABLE_SELECTIONS).where(POLL_ID,equal(pollId)).exec(db);
|
||||
var result = new Poll.Evaluation(poll);
|
||||
var rs = select(ALL).from(TABLE_SELECTIONS).where(POLL_ID,equal(poll.id())).exec(db);
|
||||
while (rs.next()) result.count(rs);
|
||||
rs.close();
|
||||
return result;
|
||||
|
||||
@@ -313,6 +313,10 @@ tr:hover .taglist .tag button {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.histo .bar{
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#app nav a{
|
||||
background: black;
|
||||
|
||||
@@ -504,6 +504,14 @@ a.wikilink{
|
||||
}
|
||||
}
|
||||
|
||||
.poll .weight .description{
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.histo {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.grid2{
|
||||
|
||||
@@ -304,6 +304,10 @@ tr:hover .taglist .tag button {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.histo .bar{
|
||||
background: #a00;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#app nav a{
|
||||
background: black;
|
||||
|
||||
@@ -619,6 +619,14 @@ a.wikilink{
|
||||
}
|
||||
}
|
||||
|
||||
.poll .weight .description{
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.histo {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.grid2,
|
||||
|
||||
@@ -283,6 +283,10 @@ tr:hover .taglist .tag button {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.histo .bar{
|
||||
background: cyan;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#app nav a{
|
||||
background: white;
|
||||
|
||||
@@ -504,6 +504,14 @@ a.wikilink{
|
||||
}
|
||||
}
|
||||
|
||||
.poll .weight .description{
|
||||
display: block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.histo {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.grid2{
|
||||
|
||||
Reference in New Issue
Block a user