diff --git a/app/DelegationOneVote.php b/app/DelegationOneVote.php index 42edc9a5f62a273be969efdf1e6054836dd09dbd..183c39966c4a48d9bbe6584a8595b6417bdff44b 100644 --- a/app/DelegationOneVote.php +++ b/app/DelegationOneVote.php @@ -28,4 +28,8 @@ class DelegationOneVote extends Model public function getFinalDelegationOneVoteAttribute() { return $this->parentDelegationOneVote()->exists() ? $this->parentDelegationOneVote->vote_direct : $this->vote_direct; } + + public function election() { + return $this->belongsTo(Election::class, 'election_id', 'id'); + } } diff --git a/app/Http/Controllers/APIController.php b/app/Http/Controllers/APIController.php index f85d04c992b600269f5d772243c3585ca56a17fb..1e5d550ff536166a520b1ad32634c967967d26d0 100644 --- a/app/Http/Controllers/APIController.php +++ b/app/Http/Controllers/APIController.php @@ -365,6 +365,12 @@ class APIController extends Controller case 'd1' : $electionMethod = 'runSingleDelegationElectionVersion1'; break; + case 'd2' : + $electionMethod = 'runSingleDelegationElectionVersion2'; + break; + case 'd3' : + $electionMethod = 'runSingleDelegationElectionVersion3'; + break; default : return response()->json('unknown election type', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -494,7 +500,7 @@ class APIController extends Controller $followersDistribution[$delegateID]++; // revert expertise choice $item['vote_direct'] ? $correctChoices-- : $incorrectChoices--; - $followersVotes[$key]['vote_direct'] = null; + //$followersVotes[$key]['vote_direct'] = null; // keep track of own expertise choice $followersVotes[$key]['vote_weight'] = 0; // add delegate choice $mappedToVoterID[$delegateID]->vote_weight++; @@ -556,6 +562,233 @@ class APIController extends Controller return $electionStats; } + private function runSingleDelegationElectionVersion2( + Population $population, + $useReputationBalance = true, + $type = 'd2', + $modifyAttributes = false + ) { + $startTime = microtime(true); + $election = Election::create([ + 'population_id' => $population->id, + 'type' => $type + ]); + + $votes = array(); + $electionStats = new \stdClass(); + $electionStats->type = "d2"; + $asDelegate = 0; + $asFollower = 0; + $asIndependent = 0; + + $weightedDelegatesIDs = array(); + + $lastInsertedWeight = 0; + $followersDistribution = array(); + + $minValue = 1; + $maxValue = 100; + $correctChoices = 0; + $incorrectChoices = 0; + + $noOfVotes = $population->voters->count(); + $electionStats->no_of_votes = $noOfVotes; + + $followersVotes = []; + + foreach ($population->voters as $voter) { + $tresholdFollowing = $voter->following; + $tresholdIndependent = 100; + $tresholdLeadership = 100 + $voter->leadership; + + $randomBehaviour = random_int(0, $tresholdLeadership); + + $randomExpertise = random_int($minValue, $maxValue); + $voteDirect = $randomExpertise <= $voter->expertise; + $voteDirect ? $correctChoices++ : $incorrectChoices++; + + $newVote = [ + 'election_id' => $election->id, + 'voter_id' => $voter->id, + 'vote_direct' => $voteDirect, + 'vote_delegation' => null, + 'vote_weight' => 1, + 'vote_final' => $voteDirect + ]; + + if ($randomBehaviour <= $tresholdFollowing) { + $newVote['voter_mark'] = 'f'; + $asFollower++; + $followersVotes[$voter->id] = $newVote; + } elseif (($randomBehaviour <= $tresholdIndependent)) { + $newVote['voter_mark'] = 'i'; + $asIndependent++; + $votes[$voter->id] = $newVote; + } else { + $newVote['voter_mark'] = 'd'; + $asDelegate++; + $reputationWeight = $voter->reputation > 0 ? $voter->reputation : 1; // minimum weight = 1 + $weightedDelegatesIDs[$voter->id] = $lastInsertedWeight + $reputationWeight; + $lastInsertedWeight = $weightedDelegatesIDs[$voter->id]; + $followersDistribution[$voter->id] = 0; + $votes[$voter->id] = $newVote; + } + } + + $electionStats->initial_correct = $correctChoices; + $electionStats->initial_incorrect = $incorrectChoices; + + $prepareTime = microtime(true); + + DelegationOneVote::insert($votes); + + $delegatesSavedVotes = array(); + + // replace own expertise test if follower and there are delegates + if ($asDelegate > 0) { + + $savedVotes = DelegationOneVote::where('election_id', '=', $election->id) + ->where('voter_mark', '=', 'd') + ->get(); + + foreach ($savedVotes as $savedVote) { + $delegatesSavedVotes[$savedVote->voter_id] = $savedVote; + } + + foreach ($followersVotes as $key => $item) { + if($item['voter_mark'] == 'f' ) { + // choose delegate + $randomDelegation = random_int(1, $lastInsertedWeight); + $delegateID = $this->findDelegateID($weightedDelegatesIDs, $randomDelegation); + + $followersVotes[$key]['vote_delegation'] = $delegatesSavedVotes[$delegateID]->id;//$delegateID; + $followersDistribution[$delegateID]++; + // revert expertise choice + $item['vote_direct'] ? $correctChoices-- : $incorrectChoices--; + //$followersVotes[$key]['vote_direct'] = null; // keep track of own expertise choice + $followersVotes[$key]['vote_weight'] = 0; + // add delegate choice + $delegatesSavedVotes[$delegateID]->vote_weight++; + $delegatesSavedVotes[$delegateID]->vote_direct ? $correctChoices++ : $incorrectChoices++; + $followersVotes[$key]['vote_final'] = $delegatesSavedVotes[$delegateID]->vote_final; + } + } + + } + + // adjust attributes of all voters (skip leadership/following adjustments) + // todo: extend data table to store initial/current leadership/following if to be adjustable over time as reputation + foreach ($population->voters as $voter) { + $previousReputation = $voter->reputation; + $voterID = $voter->id; + if (isset($delegatesSavedVotes[$voterID])) { + // is delegate + $noOfFollowers = $followersDistribution[$delegateID]; + if ($noOfFollowers > 0) { + // save weight adjustments for delegate's vote + $delegatesSavedVotes[$voterID]->save(); + // adjust voter reputation + if ($delegatesSavedVotes[$voterID]->vote_final > 0) { + $voter->reputation += (2 * $noOfFollowers); + if($modifyAttributes && $voter->leadership < 100) + $voter->leadership++; + } else { + $voter->reputation -= (2 * $noOfFollowers); + if($modifyAttributes && $voter->leadership > 1) + $voter->leadership--; + } + } else { + if($modifyAttributes && $voter->leadership > 1) + $voter->leadership--; + } + } elseif ($modifyAttributes && isset($followersVotes[$voterID])) { + // is follower + if ($followersVotes[$voterID]['vote_final']) { + if (!$followersVotes[$voterID]['vote_direct']) { + if($voter->following < 100) + $voter->following++; + } + } elseif ($followersVotes[$voterID]['vote_direct']) { + if($voter->following > 1) + $voter->following--; + } + } + + // balance voter reputation over time between elections (todo: move to another db call?) + if ($useReputationBalance) { + if ($voter->reputation < 0) { + $voter->reputation++; + } elseif ($voter->reputation > 0) { + $voter->reputation--; + } + } + if ($previousReputation != $voter->reputation) { + $voter->save(); + } + } + + $election->total_correct = $correctChoices; + $election->total_incorrect = $incorrectChoices; + + $electionStats->total_correct_choices = $correctChoices; + $electionStats->total_incorrect_choices = $incorrectChoices; + + if ($noOfVotes > 0) { + $electionStats->percent_initial_correct_choices = 100 * $electionStats->initial_correct / $noOfVotes; + $electionStats->percent_correct_choices = 100 * $election->total_correct / $noOfVotes; + } else { + $electionStats->percent_initial_correct_choices = null; + $electionStats->percent_correct_choices = null; + } + + $votesTime = microtime(true); + + $election->save(); + + $electionExtension = ExtensionDelegationElection::create([ + 'election_id' => $election->id, + 'initial_correct' => $electionStats->initial_correct, + 'initial_incorrect' => $electionStats->initial_incorrect, + 'as_delegate' => $asDelegate, + 'as_follower' => $asFollower, + 'as_independent' => $asIndependent + ]); + + DelegationOneVote::insert($followersVotes); + + $databaseTime = microtime(true); + + $electionStats->votes_time = round($votesTime - $startTime, 5); + $electionStats->votes_db_time = round($databaseTime - $votesTime, 5); + + $electionStats->as_delegate = $asDelegate; + $electionStats->as_follower = $asFollower; + $electionStats->as_independent = $asIndependent; + + $electionStats->delegates = array_keys($weightedDelegatesIDs); + $electionStats->cumulative_delegates_reputation = $weightedDelegatesIDs; + $electionStats->followers_distribution = $followersDistribution; + $electionStats->data = array_values($votes); + + return $electionStats; + } + + /** + * Modify Following and Leadership after each election + */ + private function runSingleDelegationElectionVersion3 (Population $population) { + return $this->runSingleDelegationElectionVersion2($population, true, 'd3', true); + } + + private function findDelegateID(array $weightedDelegatesIDs, int $randomDelegation) { + foreach ($weightedDelegatesIDs as $id => $cumulativeReputation) { + if ($randomDelegation <= $cumulativeReputation) { + return $id; + } + } + return null; + } + public function getVoterStats($population, $voter) { $data = Voter::where('id', '=', $voter) ->with('delegationOneVotes.parentDelegationOneVote') @@ -582,12 +815,15 @@ class APIController extends Controller try { $attributes = $request->validate([ - 'type' => 'required|string|in:m,d1' + 'type' => 'required|string|in:m,d1,d2,d3', + 'moving_average' => 'nullable|integer' ]); } catch (ValidationException $e) { return response()->json(['error' => 'Unknown election type'], Response::HTTP_UNPROCESSABLE_ENTITY); } + $movingAverage = isset($attributes['moving_average']) ? $attributes['moving_average'] : 0; + $data->elections_type = $attributes['type']; $data->no_of_voters = $population->noOfVoters; @@ -603,8 +839,26 @@ class APIController extends Controller ->pluck('percent'); $data->no_of_elections = $elections->count(); - $data->elections = $elections; + $data->moving_average = $movingAverage; + + if($movingAverage > 0) { + $flattenData = []; + if ($data->no_of_elections >= $movingAverage) { + $asArray = $elections->toArray(); + for ($i = $movingAverage - 1; $i < $data->no_of_elections; $i++) { + $sumOfValues = 0; + for ($j = 0; $j < $movingAverage; $j++) { + $sumOfValues += $asArray[$i - $j]; + } + $flattenData[] = $sumOfValues / $movingAverage; + } + } + $data->elections = $flattenData; + } else { + $data->elections = $elections; + } return response()->json($data, Response::HTTP_OK); } + } diff --git a/app/Population.php b/app/Population.php index 4898aa662cc530aa8a4fc201a69b2cd531b57b37..6b16c96639f98c4b718a579aede80511d604fdcd 100644 --- a/app/Population.php +++ b/app/Population.php @@ -67,6 +67,24 @@ class Population extends Model $percentCorrectD1 = null; } + $noOfCorrectAvgD2 = $this->elections()->where('type', '=', 'd2')->average('total_correct'); + $noOfIncorrectAvgD2 = $this->elections()->where('type', '=', 'd2')->average('total_incorrect'); + $sumD2 = $noOfCorrectAvgD2 + $noOfIncorrectAvgD2; + if ($sumD2 > 0) { + $percentCorrectD2 = 100 * $noOfCorrectAvgD2 / $sumD2; + } else { + $percentCorrectD2 = null; + } + + $noOfCorrectAvgD3 = $this->elections()->where('type', '=', 'd3')->average('total_correct'); + $noOfIncorrectAvgD3 = $this->elections()->where('type', '=', 'd3')->average('total_incorrect'); + $sumD3 = $noOfCorrectAvgD3 + $noOfIncorrectAvgD3; + if ($sumD3 > 0) { + $percentCorrectD3 = 100 * $noOfCorrectAvgD3 / $sumD3; + } else { + $percentCorrectD3 = null; + } + return [ [ 'type' => 'm', @@ -81,6 +99,20 @@ class Population extends Model 'no_of_correct_average' => $noOfCorrectAvgD1, 'no_of_incorrect_average' => $noOfIncorrectAvgD1, 'percent_correct' => $percentCorrectD1 + ], + [ + 'type' => 'd2', + 'count' => $this->elections()->where('type', '=', 'd2')->count(), + 'no_of_correct_average' => $noOfCorrectAvgD2, + 'no_of_incorrect_average' => $noOfIncorrectAvgD2, + 'percent_correct' => $percentCorrectD2 + ], + [ + 'type' => 'd3', + 'count' => $this->elections()->where('type', '=', 'd3')->count(), + 'no_of_correct_average' => $noOfCorrectAvgD3, + 'no_of_incorrect_average' => $noOfIncorrectAvgD3, + 'percent_correct' => $percentCorrectD3 ] ]; } diff --git a/app/Voter.php b/app/Voter.php index 82af8f21b886215f63b11f84b69b6e0cc828dd36..5d8cf51f2c6795aa812e20600e195806e7046388 100644 --- a/app/Voter.php +++ b/app/Voter.php @@ -17,7 +17,7 @@ class Voter extends Model 'group' ]; - protected $appends = ['majority_votes_stats', 'delegation_one_votes_stats']; + protected $appends = ['majority_votes_stats', 'delegation_one_votes_stats', 'delegation_two_votes_stats']; public function majorityVotes() { return $this->hasMany(MajorityVote::class, 'voter_id', 'id'); @@ -39,7 +39,13 @@ class Voter extends Model } public function delegationOneVotes() { - return $this->hasMany(DelegationOneVote::class, 'voter_id', 'id'); + return $this->hasMany(DelegationOneVote::class, 'voter_id', 'id') + ->whereHas('election', function($q) {$q->where('type', '=', 'd1');}); + } + + public function delegationTwoVotes() { + return $this->hasMany(DelegationOneVote::class, 'voter_id', 'id') + ->whereHas('election', function($q) {$q->where('type', '=', 'd2');}); } public function myDelegationOneVotingDelegate() { @@ -75,7 +81,7 @@ class Voter extends Model - $myDelegationOneVotesFinalsCorrectAsFollower - $myDelegationOneVotesFinalsCorrectAsIndependent; - $myDelegationOneVotesPercentFinalsCorrect = $myDelegationOneVotesFinalsCorrect / $noOfDelegationOneVotes; + $myDelegationOneVotesPercentFinalsCorrect = 100 * $myDelegationOneVotesFinalsCorrect / $noOfDelegationOneVotes; return [ 'as_independent' => $asIndependent, @@ -89,4 +95,48 @@ class Voter extends Model 'finals_correct_as_delegate' => $myDelegationOneVotesFinalsCorrectAsDelegate ]; } + + public function getDelegationTwoVotesStatsAttribute() { + $noOfDelegationTwoVotes = $this->delegationTwoVotes()->count(); + + if ($noOfDelegationTwoVotes < 1) { + return [ + 'as_independent' => null, + 'as_follower' => null, + 'as_delegate' => null, + 'percent_finals_correct' => null, + 'finals_correct' => null, + 'finals_incorrect' => null, + 'finals_correct_as_independent' => null, + 'finals_correct_as_follower' => null, + 'finals_correct_as_delegate' => null + ]; + } + + $asIndependent = $this->delegationTwoVotes()->where('voter_mark', '=', 'i')->count(); + $asFollower = $this->delegationTwoVotes()->where('voter_mark', '=', 'f')->count(); + $asDelegate = $this->delegationTwoVotes()->where('voter_mark', '=', 'd')->where('vote_weight', '>', 1)->count(); + $myDelegationTwoVotesFinalsCorrect = $this->delegationTwoVotes()->where('vote_final', '=', 1)->count(); + $myDelegationTwoVotesFinalsIncorrect = $noOfDelegationTwoVotes - $myDelegationTwoVotesFinalsCorrect; + $myDelegationTwoVotesFinalsCorrectAsFollower = $this->delegationTwoVotes()->where('vote_final', '=', 1)->where('voter_mark', '=', 'f')->count(); + $myDelegationTwoVotesFinalsCorrectAsIndependent = $this->delegationTwoVotes()->where('vote_final', '=', 1)->where('voter_mark', '=', 'i')->count(); + + $myDelegationTwoVotesFinalsCorrectAsDelegate = $myDelegationTwoVotesFinalsCorrect + - $myDelegationTwoVotesFinalsCorrectAsFollower + - $myDelegationTwoVotesFinalsCorrectAsIndependent; + + $myDelegationTwoVotesPercentFinalsCorrect = 100 * $myDelegationTwoVotesFinalsCorrect / $noOfDelegationTwoVotes; + + return [ + 'as_independent' => $asIndependent, + 'as_follower' => $asFollower, + 'as_delegate' => $asDelegate, + 'percent_finals_correct' => $myDelegationTwoVotesPercentFinalsCorrect, + 'finals_correct' => $myDelegationTwoVotesFinalsCorrect, + 'finals_incorrect' => $myDelegationTwoVotesFinalsIncorrect, + 'finals_correct_as_independent' => $myDelegationTwoVotesFinalsCorrectAsIndependent, + 'finals_correct_as_follower' => $myDelegationTwoVotesFinalsCorrectAsFollower, + 'finals_correct_as_delegate' => $myDelegationTwoVotesFinalsCorrectAsDelegate + ]; + } } diff --git a/database/migrations/2021_03_24_161037_add_reputation_to_voters_table.php b/database/migrations/2021_03_24_161037_add_reputation_to_voters_table.php new file mode 100644 index 0000000000000000000000000000000000000000..11f8e050a524fce1c598fc94d0112c95ffd808af --- /dev/null +++ b/database/migrations/2021_03_24_161037_add_reputation_to_voters_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class AddReputationToVotersTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::table('voters', function (Blueprint $table) { + $table->integer('reputation', false, false)->default(0)->after('leadership'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('voters', function (Blueprint $table) { + $table->dropColumn('reputation'); + }); + } +} diff --git a/public/js/app.js b/public/js/app.js index 512cb789bf26483b204ced0b1386a18b085959b7..d492cf8e733e24a258e236772937d8768e936243 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2453,6 +2453,34 @@ __webpack_require__.r(__webpack_exports__); // // // +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// /* harmony default export */ __webpack_exports__["default"] = ({ @@ -2537,7 +2565,8 @@ __webpack_require__.r(__webpack_exports__); groups.push({ label: 'group:' + voters_group.name, backgroundColor: colors[color_idx], - data: voters_group_set + data: voters_group_set, + yAxisID: 'left-y-axis' }); color_idx = color_idx < 4 ? color_idx + 1 : 0; }); @@ -2849,6 +2878,19 @@ __webpack_require__.r(__webpack_exports__); // // // +// +// +// +// +// +// +// +// +// +// +// +// +// @@ -2862,6 +2904,7 @@ __webpack_require__.r(__webpack_exports__); }, data: function data() { return { + custom_number_elections: 1, current_election_timeline_key: null, election_timeline_selector: [{ value: 'm', @@ -2869,13 +2912,21 @@ __webpack_require__.r(__webpack_exports__); }, { value: 'd1', text: 'Delegation version 1 (d1)' + }, { + value: 'd2', + text: 'Delegation version 2 (d2)' + }, { + value: 'd3', + text: 'Delegation version 3 (d3)' }], elections_timeline: null, + moving_average: 0, auto_fetch_elections_timeline: false, show_timeline_graph: true, running_elections_lock: false, auto_fetch_voters: false, - show_voters_graph: true, + show_voters_graph: false, + show_last_election_chart: false, auto_fetch_distribution: false, feedback: null, population_id: route().params.population_id, @@ -2917,6 +2968,11 @@ __webpack_require__.r(__webpack_exports__); height: '300px', width: '100%', position: 'relative' + }, + h_500_chart_styles: { + height: '500px', + width: '100%', + position: 'relative' } }; }, @@ -2985,11 +3041,16 @@ __webpack_require__.r(__webpack_exports__); var confidence = []; var following = []; var leadership = []; + var reputation = []; var m_percent_correct = []; var d1_percent_correct = []; - var as_independent = []; - var as_follower = []; - var as_delegate = []; + var d1_as_independent = []; + var d1_as_follower = []; + var d1_as_delegate = []; + var d2_percent_correct = []; + var d2_as_independent = []; + var d2_as_follower = []; + var d2_as_delegate = []; var diff = []; this.voters.forEach(function (value, idx) { labels.push(idx); @@ -2997,12 +3058,17 @@ __webpack_require__.r(__webpack_exports__); confidence.push(value.confidence); following.push(value.following); leadership.push(value.leadership); + reputation.push(value.reputation); m_percent_correct.push(value.majority_votes_stats.percent_correct); //diff.push(value.majority_votes_stats.percent_correct - value.expertise); - d1_percent_correct.push(value.majority_votes_stats.percent_correct); - as_independent.push(value.delegation_one_votes_stats.as_independent); - as_follower.push(value.delegation_one_votes_stats.as_follower); - as_delegate.push(value.delegation_one_votes_stats.as_delegate); + d1_percent_correct.push(value.delegation_one_votes_stats.percent_finals_correct); + d1_as_independent.push(value.delegation_one_votes_stats.as_independent); + d1_as_follower.push(value.delegation_one_votes_stats.as_follower); + d1_as_delegate.push(value.delegation_one_votes_stats.as_delegate); + d2_percent_correct.push(value.delegation_two_votes_stats.percent_finals_correct); + d2_as_independent.push(value.delegation_two_votes_stats.as_independent); + d2_as_follower.push(value.delegation_two_votes_stats.as_follower); + d2_as_delegate.push(value.delegation_two_votes_stats.as_delegate); }); return { labels: labels, @@ -3026,13 +3092,13 @@ __webpack_require__.r(__webpack_exports__); yAxisID: 'left-y-axis' }, { label: 'leadership', - borderColor: '#01439b', + borderColor: '#2f779b', fill: false, data: leadership, yAxisID: 'left-y-axis' }, { label: 'percent correct (M)', - borderColor: '#9b4e44', + borderColor: '#9a9b69', fill: false, data: m_percent_correct, yAxisID: 'left-y-axis' @@ -3055,19 +3121,49 @@ __webpack_require__.r(__webpack_exports__); label: 'as independent (D1)', borderColor: '#ebf04b', fill: false, - data: as_independent, + data: d1_as_independent, yAxisID: 'right-y-axis' }, { label: 'as follower (D1)', borderColor: '#ffe136', fill: false, - data: as_follower, + data: d1_as_follower, yAxisID: 'right-y-axis' }, { label: 'as delegate (D1)', borderColor: '#b7b30e', fill: false, - data: as_delegate, + data: d1_as_delegate, + yAxisID: 'right-y-axis' + }, { + label: 'percent correct (D2)', + borderColor: '#964625', + fill: false, + data: d2_percent_correct, + yAxisID: 'left-y-axis' + }, { + label: 'as independent (D2)', + borderColor: '#f0ba4e', + fill: false, + data: d2_as_independent, + yAxisID: 'right-y-axis' + }, { + label: 'as follower (D2)', + borderColor: '#ff8843', + fill: false, + data: d2_as_follower, + yAxisID: 'right-y-axis' + }, { + label: 'as delegate (with followers) (D2)', + borderColor: '#b75135', + fill: false, + data: d2_as_delegate, + yAxisID: 'right-y-axis' + }, { + label: 'reputation', + borderColor: '#000000', + fill: false, + data: reputation, yAxisID: 'right-y-axis' }] }; @@ -3267,7 +3363,8 @@ __webpack_require__.r(__webpack_exports__); axios.get(route('internal.api.population.get.elections.timeline', this.population_id), { params: { - 'type': this.current_election_timeline_key.value + 'type': this.current_election_timeline_key.value, + 'moving_average': this.moving_average } }).then(function (response) { _this5.elections_timeline = response.data; @@ -78959,16 +79056,19 @@ var render = function() { _vm._v( "\n " + _vm._s(population.name) + - "\n " + " " ), _c("i", [ _vm._v( "voters: " + _vm._s(population.voters_stats.no_of_voters) + - ", elections: " + "," ) ]), _vm._v(" "), + _c("br"), + _c("i", [_vm._v("elections: ")]), + _vm._v(" "), _vm._l(population.elections_stats, function(election) { return _c("i", [ _vm._v( @@ -79026,7 +79126,22 @@ var render = function() { attrs: { "chart-data": _vm.population_election_stats_chart_data, - options: { maintainAspectRatio: false }, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [ + { + id: "left-y-axis", + type: "linear", + position: "left", + ticks: { + min: 0, + max: 100 + } + } + ] + } + }, styles: { height: 200 } } }) @@ -79052,7 +79167,22 @@ var render = function() { attrs: { "chart-data": _vm.population_voters_stats_chart_data, - options: { maintainAspectRatio: false }, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [ + { + id: "left-y-axis", + type: "linear", + position: "left", + ticks: { + min: 0, + max: 100 + } + } + ] + } + }, styles: { height: 200 } } }) @@ -79112,7 +79242,7 @@ var render = function() { var _c = _vm._self._c || _h return _c("div", { staticClass: "p-2" }, [ _c("div", { staticClass: "row" }, [ - _c("div", { staticClass: "col-md-2 col-lg-2" }, [ + _c("div", { staticClass: "col-md-3 col-lg-3" }, [ _c("div", { staticClass: "col-md-12" }, [ _c("div", { staticClass: "card" }, [ _c("div", { staticClass: "card-header" }, [ @@ -79121,31 +79251,35 @@ var render = function() { _vm._v(" "), _c("div", { staticClass: "card-body" }, [ _c("div", [ - _vm._v("\n Majority elections:"), - _c("br"), - _vm._v(" "), - _c("i", { staticClass: "text-muted text-sm-left" }, [ - _vm._v("Based on own Expertise.") + _c("label", { staticClass: "text-info" }, [ + _vm._v("Number of elections: ") ]), - _c("br"), _vm._v(" "), - _c( - "button", - { - staticClass: "btn btn-sm btn-outline-info", - attrs: { disabled: _vm.running_elections_lock }, - on: { - click: function($event) { - $event.preventDefault() - return _vm.runElections("m", 1) + _c("input", { + directives: [ + { + name: "model", + rawName: "v-model", + value: _vm.custom_number_elections, + expression: "custom_number_elections" + } + ], + staticStyle: { width: "70px" }, + attrs: { type: "number", min: "1", max: "100", step: "0" }, + domProps: { value: _vm.custom_number_elections }, + on: { + input: function($event) { + if ($event.target.composing) { + return } + _vm.custom_number_elections = $event.target.value } - }, - [ - _vm._v("\n Run 1 election "), - _c("i", [_vm._v("(type m)")]) - ] - ), + } + }) + ]), + _vm._v(" "), + _c("div", [ + _c("h5", [_vm._v("Majority elections:")]), _vm._v(" "), _c( "button", @@ -79155,18 +79289,29 @@ var render = function() { on: { click: function($event) { $event.preventDefault() - return _vm.runElections("m", 5) + return _vm.runElections( + "m", + _vm.custom_number_elections + ) } } }, [ _vm._v( - "\n Run 5 elections " + "\n Run " + + _vm._s(_vm.custom_number_elections) + + " election" ), + _vm.custom_number_elections > 1 + ? _c("span", [_vm._v("s")]) + : _vm._e(), + _vm._v(" "), _c("i", [_vm._v("(type m)")]) ] ), _vm._v(" "), + _c("br"), + _vm._v(" "), _c( "button", { @@ -79175,23 +79320,22 @@ var render = function() { on: { click: function($event) { $event.preventDefault() - return _vm.runElections("m", 10) + return _vm.fetchMajorityElectionsDistribution($event) } } }, - [ - _vm._v( - "\n Run 10 elections " - ), - _c("i", [_vm._v("(type m)")]) - ] + [_vm._v("Fetch majority elections distribution")] ) ]), _vm._v(" "), + _c("hr"), + _vm._v(" "), + _vm._m(0), + _vm._v(" "), _c("div", [ - _vm._v( - "\n Majority elections distribution:" - ), + _vm._v("\n Delegation elections"), + _c("i", [_vm._v("(type d1)")]), + _vm._v(" :"), _c("br"), _vm._v(" "), _c( @@ -79202,24 +79346,36 @@ var render = function() { on: { click: function($event) { $event.preventDefault() - return _vm.fetchMajorityElectionsDistribution($event) + return _vm.runElections( + "d1", + _vm.custom_number_elections + ) } } }, - [_vm._v("Fetch majority elections distribution")] + [ + _vm._v( + "\n Run " + + _vm._s(_vm.custom_number_elections) + + " election" + ), + _vm.custom_number_elections > 1 + ? _c("span", [_vm._v("s")]) + : _vm._e(), + _vm._v(" "), + _c("i", [_vm._v("(type d1)")]) + ] ) ]), _vm._v(" "), _c("div", [ _vm._v("\n Delegation elections "), - _c("i", [_vm._v("(type d1)")]), + _c("i", [_vm._v("(type d2)")]), _vm._v(" :"), _c("br"), _vm._v(" "), _c("i", { staticClass: "text-muted text-sm-left" }, [ - _vm._v( - "Three options, being: delegate/follower/independent (chance based on Leadership and Following), delegates and independents use own Expertise (single delegation level)." - ) + _vm._v("Reputation included") ]), _c("br"), _vm._v(" "), @@ -79231,35 +79387,40 @@ var render = function() { on: { click: function($event) { $event.preventDefault() - return _vm.runElections("d1", 1) - } - } - }, - [ - _vm._v("\n Run 1 election "), - _c("i", [_vm._v("(type d1)")]) - ] - ), - _vm._v(" "), - _c( - "button", - { - staticClass: "btn btn-sm btn-outline-info", - attrs: { disabled: _vm.running_elections_lock }, - on: { - click: function($event) { - $event.preventDefault() - return _vm.runElections("d1", 5) + return _vm.runElections( + "d2", + _vm.custom_number_elections + ) } } }, [ _vm._v( - "\n Run 5 elections " + "\n Run " + + _vm._s(_vm.custom_number_elections) + + " election" ), - _c("i", [_vm._v("(type d1)")]) + _vm.custom_number_elections > 1 + ? _c("span", [_vm._v("s")]) + : _vm._e(), + _vm._v(" "), + _c("i", [_vm._v("(type d2)")]) ] - ), + ) + ]), + _vm._v(" "), + _c("div", [ + _vm._v("\n Delegation elections "), + _c("i", [_vm._v("(type d3)")]), + _vm._v(" :"), + _c("br"), + _vm._v(" "), + _c("i", { staticClass: "text-muted text-sm-left" }, [ + _vm._v( + "Reputation included. Following and Leadership attributes may change for each election !! (do not use together with d1 or d2 on one population)" + ) + ]), + _c("br"), _vm._v(" "), _c( "button", @@ -79269,15 +79430,24 @@ var render = function() { on: { click: function($event) { $event.preventDefault() - return _vm.runElections("d1", 10) + return _vm.runElections( + "d3", + _vm.custom_number_elections + ) } } }, [ _vm._v( - "\n Run 10 elections " + "\n Run " + + _vm._s(_vm.custom_number_elections) + + " election" ), - _c("i", [_vm._v("(type d1)")]) + _vm.custom_number_elections > 1 + ? _c("span", [_vm._v("s")]) + : _vm._e(), + _vm._v(" "), + _c("i", [_vm._v("(type d3)")]) ] ) ]) @@ -79286,7 +79456,7 @@ var render = function() { ]) ]), _vm._v(" "), - _c("div", { staticClass: "col-md-10 col-lg-10" }, [ + _c("div", { staticClass: "col-md-9 col-lg-9" }, [ _c("div", { staticClass: "row" }, [ _vm.feedback ? _c( @@ -79575,6 +79745,34 @@ var render = function() { } } } + }), + _vm._v(" "), + _c("br"), + _vm._v(" "), + _c("label", { staticClass: "text-info" }, [ + _vm._v("Moving average") + ]), + _vm._v(" "), + _c("input", { + directives: [ + { + name: "model", + rawName: "v-model", + value: _vm.moving_average, + expression: "moving_average" + } + ], + staticStyle: { width: "70px" }, + attrs: { type: "number", min: "0", step: "1" }, + domProps: { value: _vm.moving_average }, + on: { + input: function($event) { + if ($event.target.composing) { + return + } + _vm.moving_average = $event.target.value + } + } }) ]), _vm._v(" "), @@ -79676,7 +79874,7 @@ var render = function() { ] } }, - styles: _vm.h_300_chart_styles + styles: _vm.h_500_chart_styles } }) ], @@ -79975,25 +80173,78 @@ var render = function() { ]) ]), _vm._v(" "), - _c("bar-chart", { - attrs: { - "chart-data": _vm.last_elections_chart_data, - options: { - maintainAspectRatio: false, - scales: { - yAxes: [ - { - id: "left-y-axis", - type: "linear", - position: "left", - ticks: { min: 0 } - } - ] + _c("label", { staticClass: "text-info" }, [ + _vm._v("Show last elections chart") + ]), + _vm._v(" "), + _c("input", { + directives: [ + { + name: "model", + rawName: "v-model", + value: _vm.show_last_election_chart, + expression: "show_last_election_chart" + } + ], + attrs: { type: "checkbox" }, + domProps: { + checked: Array.isArray( + _vm.show_last_election_chart + ) + ? _vm._i( + _vm.show_last_election_chart, + null + ) > -1 + : _vm.show_last_election_chart + }, + on: { + change: function($event) { + var $$a = _vm.show_last_election_chart, + $$el = $event.target, + $$c = $$el.checked ? true : false + if (Array.isArray($$a)) { + var $$v = null, + $$i = _vm._i($$a, $$v) + if ($$el.checked) { + $$i < 0 && + (_vm.show_last_election_chart = $$a.concat( + [$$v] + )) + } else { + $$i > -1 && + (_vm.show_last_election_chart = $$a + .slice(0, $$i) + .concat($$a.slice($$i + 1))) + } + } else { + _vm.show_last_election_chart = $$c } - }, - styles: { height: 200 } + } } - }) + }), + _vm._v(" "), + _vm.show_last_election_chart + ? _c("bar-chart", { + attrs: { + "chart-data": + _vm.last_elections_chart_data, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [ + { + id: "left-y-axis", + type: "linear", + position: "left", + ticks: { min: 0 } + } + ] + } + }, + styles: { height: 200 } + } + }) + : _vm._e() ], 1 ) @@ -80011,7 +80262,23 @@ var render = function() { ]) ]) } -var staticRenderFns = [] +var staticRenderFns = [ + function() { + var _vm = this + var _h = _vm.$createElement + var _c = _vm._self._c || _h + return _c("div", [ + _c("h5", [_vm._v("Delegation elections")]), + _vm._v(" "), + _c("i", { staticClass: "text-muted text-sm-left" }, [ + _vm._v( + "Three options, being: delegate/follower/independent (chance based on Leadership and Following), delegates and independents use own Expertise (single delegation level)." + ) + ]), + _c("br") + ]) + } +] render._withStripped = true diff --git a/resources/js/components/population-index.vue b/resources/js/components/population-index.vue index 18b216fc465a31c628e9d2c184679f4de227f676..ae8b192e73bf00b6a76549ada754d2765025bca3 100644 --- a/resources/js/components/population-index.vue +++ b/resources/js/components/population-index.vue @@ -37,8 +37,8 @@ class="btn btn-outline-info" v-bind:class="{'btn-info text-white': (current_population != null && current_population.id == population.id)}" > - {{population.name}} - <i>voters: {{population.voters_stats.no_of_voters}}, elections: </i> + {{population.name}} <i>voters: {{population.voters_stats.no_of_voters}},</i> + <br><i>elections: </i> <i v-for="election in population.elections_stats">(type-{{election.type}}: # {{election.count}})</i> </span> </div> @@ -60,14 +60,42 @@ <div class="card"> <div class="card-header">Election stats</div> <div v-if="current_population.elections_stats" class="card-body"> - <bar-chart :chart-data="population_election_stats_chart_data" :options="{maintainAspectRatio: false}" :styles="{height: 200}"></bar-chart> + <bar-chart :chart-data="population_election_stats_chart_data" + :options="{ + maintainAspectRatio: false, + scales: { + yAxes: [{ + id: 'left-y-axis', + type: 'linear', + position: 'left', + ticks: { + min: 0, + max: 100 + } + }] + }}" + :styles="{height: 200}"></bar-chart> </div> <div v-else class="card-body"><i>N/A</i></div> </div> <div class="card"> <div class="card-header">Voters stats</div> <div v-if="current_population.voters_stats" class="card-body"> - <bar-chart :chart-data="population_voters_stats_chart_data" :options="{maintainAspectRatio: false}" :styles="{height: 200}"></bar-chart> + <bar-chart :chart-data="population_voters_stats_chart_data" + :options="{ + maintainAspectRatio: false, + scales: { + yAxes: [{ + id: 'left-y-axis', + type: 'linear', + position: 'left', + ticks: { + min: 0, + max: 100 + } + }] + }}" + :styles="{height: 200}"></bar-chart> </div> <div v-else class="card-body"><i>N/A</i></div> </div> @@ -176,7 +204,8 @@ groups.push({ label: 'group:' + voters_group.name, backgroundColor: colors[color_idx], - data: voters_group_set + data: voters_group_set, + yAxisID: 'left-y-axis' }); color_idx = color_idx < 4 ? color_idx + 1 : 0; }); diff --git a/resources/js/components/population-show.vue b/resources/js/components/population-show.vue index 23c5ec33929dab9968ba27839d23177e52570201..a9aab02cd97080ddc2dab3a3d8e4e11ba9a0d989 100644 --- a/resources/js/components/population-show.vue +++ b/resources/js/components/population-show.vue @@ -1,46 +1,53 @@ <template> <div class="p-2"> <div class="row"> - <div class="col-md-2 col-lg-2"> + <div class="col-md-3 col-lg-3"> <div class="col-md-12"> <div class="card"> <div class="card-header">{{population_name}} Actions</div> <div class="card-body"> <div> - Majority elections:<br> - <i class="text-muted text-sm-left">Based on own Expertise.</i><br> - <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('m', 1)"> - Run 1 election <i>(type m)</i> - </button> - <button :disabled="running_elections_lock"class="btn btn-sm btn-outline-info" @click.prevent="runElections('m', 5)"> - Run 5 elections <i>(type m)</i> - </button> - <button :disabled="running_elections_lock"class="btn btn-sm btn-outline-info" @click.prevent="runElections('m', 10)"> - Run 10 elections <i>(type m)</i> - </button> + <label class="text-info">Number of elections: </label> + <input type="number" min="1" max="100" step="0" v-model="custom_number_elections" style="width:70px"> </div> <div> - Majority elections distribution:<br> + <h5>Majority elections:</h5> + <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('m', custom_number_elections)"> + Run {{custom_number_elections}} election<span v-if="custom_number_elections > 1">s</span> <i>(type m)</i> + </button> + <br> <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="fetchMajorityElectionsDistribution">Fetch majority elections distribution</button> </div> + <hr> <div> - Delegation elections <i>(type d1)</i> :<br> + <h5>Delegation elections</h5> <i class="text-muted text-sm-left">Three options, being: delegate/follower/independent (chance based on Leadership and Following), delegates and independents use own Expertise (single delegation level).</i><br> - <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('d1', 1)"> - Run 1 election <i>(type d1)</i> + </div> + <div> + Delegation elections<i>(type d1)</i> :<br> + <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('d1', custom_number_elections)"> + Run {{custom_number_elections}} election<span v-if="custom_number_elections > 1">s</span> <i>(type d1)</i> </button> - <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('d1', 5)"> - Run 5 elections <i>(type d1)</i> + </div> + <div> + Delegation elections <i>(type d2)</i> :<br> + <i class="text-muted text-sm-left">Reputation included</i><br> + <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('d2', custom_number_elections)"> + Run {{custom_number_elections}} election<span v-if="custom_number_elections > 1">s</span> <i>(type d2)</i> </button> - <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('d1', 10)"> - Run 10 elections <i>(type d1)</i> + </div> + <div> + Delegation elections <i>(type d3)</i> :<br> + <i class="text-muted text-sm-left">Reputation included. Following and Leadership attributes may change for each election !! (do not use together with d1 or d2 on one population)</i><br> + <button :disabled="running_elections_lock" class="btn btn-sm btn-outline-info" @click.prevent="runElections('d3', custom_number_elections)"> + Run {{custom_number_elections}} election<span v-if="custom_number_elections > 1">s</span> <i>(type d3)</i> </button> </div> </div> </div> </div> </div> - <div class="col-md-10 col-lg-10"> + <div class="col-md-9 col-lg-9"> <div class="row"> <div class="alert alert-info col-md-12 col-lg-12" v-if="feedback"> INFO: {{feedback}} @@ -105,6 +112,9 @@ <div class="col-md-4"> <label class="text-info">Auto update timeline after election</label> <input type="checkbox" v-model="auto_fetch_elections_timeline"> + <br> + <label class="text-info">Moving average</label> + <input type="number" min="0" step="1" v-model="moving_average" style="width:70px"> </div> <div class="col-md-4"> <label class="text-info">Show timeline graph</label> @@ -124,7 +134,7 @@ yAxes: [{id: 'left-y-axis',type: 'linear',position: 'left',ticks: {min: 0, max:100}}] } }" - :styles="h_300_chart_styles" + :styles="h_500_chart_styles" ></line-chart> </div> <div v-else> @@ -221,7 +231,10 @@ <br> Total time: <i>{{last_elections_data.total_time}}</i> </div> - <bar-chart :chart-data="last_elections_chart_data" + <label class="text-info">Show last elections chart</label> + <input type="checkbox" v-model="show_last_election_chart"> + <bar-chart v-if="show_last_election_chart" + :chart-data="last_elections_chart_data" :options="{ maintainAspectRatio: false, scales: { @@ -256,6 +269,7 @@ components: {LineChart, BarChart, vSelect}, data() { return { + custom_number_elections: 1, current_election_timeline_key: null, election_timeline_selector : [ { @@ -265,14 +279,24 @@ { value: 'd1', text: 'Delegation version 1 (d1)' + }, + { + value: 'd2', + text: 'Delegation version 2 (d2)' + }, + { + value: 'd3', + text: 'Delegation version 3 (d3)' } ], elections_timeline: null, + moving_average: 0, auto_fetch_elections_timeline: false, show_timeline_graph: true, running_elections_lock: false, auto_fetch_voters: false, - show_voters_graph: true, + show_voters_graph: false, + show_last_election_chart: false, auto_fetch_distribution: false, feedback : null, population_id: route().params.population_id, @@ -312,6 +336,11 @@ height: '300px', width: '100%', position: 'relative' + }, + h_500_chart_styles: { + height: '500px', + width: '100%', + position: 'relative' } } }, @@ -395,11 +424,16 @@ let confidence = []; let following = []; let leadership = []; + let reputation = []; let m_percent_correct = []; let d1_percent_correct = []; - let as_independent = []; - let as_follower = []; - let as_delegate = []; + let d1_as_independent = []; + let d1_as_follower = []; + let d1_as_delegate = []; + let d2_percent_correct = []; + let d2_as_independent = []; + let d2_as_follower = []; + let d2_as_delegate = []; let diff = []; this.voters.forEach((value, idx) => { labels.push(idx); @@ -407,12 +441,17 @@ confidence.push(value.confidence); following.push(value.following); leadership.push(value.leadership); + reputation.push(value.reputation); m_percent_correct.push(value.majority_votes_stats.percent_correct); //diff.push(value.majority_votes_stats.percent_correct - value.expertise); - d1_percent_correct.push(value.majority_votes_stats.percent_correct); - as_independent.push(value.delegation_one_votes_stats.as_independent); - as_follower.push(value.delegation_one_votes_stats.as_follower); - as_delegate.push(value.delegation_one_votes_stats.as_delegate); + d1_percent_correct.push(value.delegation_one_votes_stats.percent_finals_correct); + d1_as_independent.push(value.delegation_one_votes_stats.as_independent); + d1_as_follower.push(value.delegation_one_votes_stats.as_follower); + d1_as_delegate.push(value.delegation_one_votes_stats.as_delegate); + d2_percent_correct.push(value.delegation_two_votes_stats.percent_finals_correct); + d2_as_independent.push(value.delegation_two_votes_stats.as_independent); + d2_as_follower.push(value.delegation_two_votes_stats.as_follower); + d2_as_delegate.push(value.delegation_two_votes_stats.as_delegate); }); return { labels: labels, @@ -440,14 +479,14 @@ }, { label: 'leadership', - borderColor: '#01439b', + borderColor: '#2f779b', fill: false, data: leadership, yAxisID: 'left-y-axis' }, { label: 'percent correct (M)', - borderColor: '#9b4e44', + borderColor: '#9a9b69', fill: false, data: m_percent_correct, yAxisID: 'left-y-axis' @@ -470,21 +509,56 @@ label: 'as independent (D1)', borderColor: '#ebf04b', fill: false, - data: as_independent, + data: d1_as_independent, yAxisID: 'right-y-axis' }, { label: 'as follower (D1)', borderColor: '#ffe136', fill: false, - data: as_follower, + data: d1_as_follower, yAxisID: 'right-y-axis' }, { label: 'as delegate (D1)', borderColor: '#b7b30e', fill: false, - data: as_delegate, + data: d1_as_delegate, + yAxisID: 'right-y-axis' + }, + { + label: 'percent correct (D2)', + borderColor: '#964625', + fill: false, + data: d2_percent_correct, + yAxisID: 'left-y-axis' + }, + { + label: 'as independent (D2)', + borderColor: '#f0ba4e', + fill: false, + data: d2_as_independent, + yAxisID: 'right-y-axis' + }, + { + label: 'as follower (D2)', + borderColor: '#ff8843', + fill: false, + data: d2_as_follower, + yAxisID: 'right-y-axis' + }, + { + label: 'as delegate (with followers) (D2)', + borderColor: '#b75135', + fill: false, + data: d2_as_delegate, + yAxisID: 'right-y-axis' + }, + { + label: 'reputation', + borderColor: '#000000', + fill: false, + data: reputation, yAxisID: 'right-y-axis' } ] @@ -680,7 +754,10 @@ }, fetchElectionsTimeline() { axios.get(route('internal.api.population.get.elections.timeline', this.population_id), { - params: {'type': this.current_election_timeline_key.value} + params: { + 'type': this.current_election_timeline_key.value, + 'moving_average': this.moving_average + } }).then((response) => { this.elections_timeline = response.data; this.feedback = 'election timeline fetched'; diff --git a/routes/api_external.php b/routes/api_external.php index 3aab822a53aeb4bb7f221f4b80276e7e9aea5118..3e1d823131a2b0adeb4c6d1b4a8507a07589882b 100644 --- a/routes/api_external.php +++ b/routes/api_external.php @@ -9,4 +9,16 @@ use Illuminate\Support\Facades\Route; | External routes no auth middleware, access check in controllers | */ +Route::post('/populations/{population}/elections','APIController@runElections') + ->name('external.api.population.election.run'); +Route::get('/populations/{population}/voters/{voter}','APIController@getVoterStats') + ->name('external.api.population.get.voter'); +Route::get('/populations/{population}/voters','APIController@getVotersStats') + ->name('external.api.population.get.voters'); +Route::get('/populations/{population}','APIController@getPopulation') + ->name('external.api.population.get'); +Route::get('/populations','APIController@getPopulations') + ->name('external.api.population.index'); + +Route::get('/populations/{population}/timeline','APIController@getElectionsTimeline');