overhauled histograms
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
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_DATE = "start_date";
|
||||||
public static final String START_TIME = "start_time";
|
public static final String START_TIME = "start_time";
|
||||||
public static final String STATE = "state";
|
public static final String STATE = "state";
|
||||||
|
public static final String STATS = "stats";
|
||||||
public static final String STATUS = "status";
|
public static final String STATUS = "status";
|
||||||
public static final String STATUS_CODE = "code";
|
public static final String STATUS_CODE = "code";
|
||||||
public static final String SUBJECT = "subject";
|
public static final String SUBJECT = "subject";
|
||||||
|
|||||||
@@ -107,18 +107,79 @@ public class Poll implements Mappable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
public static class Evaluation {
|
public static class Evaluation {
|
||||||
// Map<Option → Map<Weight → Count>>
|
private static class Histogram extends HashMap<Integer,Integer>{
|
||||||
private HashMap<Integer,Map<Integer,Integer>> selections = new HashMap<>();
|
public Double average() {
|
||||||
public void count(ResultSet rs) throws SQLException {
|
var sum = 0;
|
||||||
var optionId = rs.getInt(OPTION_ID);
|
var count = 0;
|
||||||
var userId = rs.getObject(USER);
|
for (var entry : entrySet()){
|
||||||
var weight = rs.getInt(WEIGHT);
|
var weight = entry.getKey();
|
||||||
var optionStats = selections.computeIfAbsent(optionId, k -> new HashMap<>());
|
var votes = entry.getValue();
|
||||||
optionStats.compute(weight, (k, sum) -> sum == null ? 1 : sum + 1);
|
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>
|
<script>
|
||||||
|
import Histogram from '../../Components/Histogram.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { api, get } from '../../urls.svelte';
|
import { api, get } from '../../urls.svelte';
|
||||||
import { error, yikes } from '../../warn.svelte';
|
import { error, yikes } from '../../warn.svelte';
|
||||||
@@ -62,25 +63,22 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{poll.options[option_id].name}
|
{poll.options[option_id].name}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{average(hist)}
|
{(+avg).toFixed(3)}
|
||||||
</td>
|
</td>
|
||||||
<td class="histogram">
|
<td>
|
||||||
{#each Object.entries(hist).sort((a,b) => a[0] - b[0]) as [weight,count]}
|
<Histogram data={histo} />
|
||||||
<span style="height: {100*count/max_val(hist)}%" title={t('voted {count} times',{count:count})}>
|
|
||||||
<span>{weight}</span>
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="warn">TODO: sort by average</span>
|
|
||||||
@@ -11,7 +11,7 @@ import java.util.Map;
|
|||||||
public interface PollDb {
|
public interface PollDb {
|
||||||
Collection<Poll> listPolls(UmbrellaUser user);
|
Collection<Poll> listPolls(UmbrellaUser user);
|
||||||
|
|
||||||
Poll.Evaluation loadEvaluation(String id);
|
Poll.Evaluation loadEvaluation(Poll poll);
|
||||||
|
|
||||||
Poll loadPoll(String id);
|
Poll loadPoll(String id);
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public class PollModule extends BaseHandler implements PollService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var result = new HashMap<>(poll.toMap());
|
var result = new HashMap<>(poll.toMap());
|
||||||
var evaluation = pollDb.loadEvaluation(poll.id());
|
var evaluation = pollDb.loadEvaluation(poll);
|
||||||
result.put(Field.EVALUATION,evaluation.toMap());
|
result.put(Field.EVALUATION,evaluation.toMap());
|
||||||
return sendContent(ex,result);
|
return sendContent(ex,result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,10 +150,10 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Poll.Evaluation loadEvaluation(String pollId) {
|
public Poll.Evaluation loadEvaluation(Poll poll) {
|
||||||
try {
|
try {
|
||||||
var result = new Poll.Evaluation();
|
var result = new Poll.Evaluation(poll);
|
||||||
var rs = select(ALL).from(TABLE_SELECTIONS).where(POLL_ID,equal(pollId)).exec(db);
|
var rs = select(ALL).from(TABLE_SELECTIONS).where(POLL_ID,equal(poll.id())).exec(db);
|
||||||
while (rs.next()) result.count(rs);
|
while (rs.next()) result.count(rs);
|
||||||
rs.close();
|
rs.close();
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -313,6 +313,10 @@ tr:hover .taglist .tag button {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histo .bar{
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: black;
|
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) {
|
@media screen and (max-width: 600px) {
|
||||||
.grid2{
|
.grid2{
|
||||||
|
|||||||
@@ -304,6 +304,10 @@ tr:hover .taglist .tag button {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histo .bar{
|
||||||
|
background: #a00;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: black;
|
background: black;
|
||||||
|
|||||||
@@ -624,6 +624,10 @@ a.wikilink{
|
|||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histo {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.grid2,
|
.grid2,
|
||||||
.grid3{
|
.grid3{
|
||||||
|
|||||||
@@ -283,6 +283,10 @@ tr:hover .taglist .tag button {
|
|||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.histo .bar{
|
||||||
|
background: cyan;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: white;
|
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) {
|
@media screen and (max-width: 600px) {
|
||||||
.grid2{
|
.grid2{
|
||||||
|
|||||||
Reference in New Issue
Block a user