diff --git a/app/Http/Controllers/APIController.php b/app/Http/Controllers/APIController.php index a3d566d363f3d9fe274df6d89af072341a01d063..cad4a0c57ecb93e00f03df5e4c36e09e36a66d56 100644 --- a/app/Http/Controllers/APIController.php +++ b/app/Http/Controllers/APIController.php @@ -986,6 +986,7 @@ class APIController extends Controller ], Response::HTTP_UNPROCESSABLE_ENTITY); } $type = $attributes['election_type']; + $metadata->election_type = $type; $templatePopulation = Population::where('id', '=', $template) ->whereNull('parent_id') ->with(['childPopulations' => function ($q) use ($type) { @@ -1081,6 +1082,7 @@ class APIController extends Controller $weightShareB = $weightB / ($weightA + $weightB); $weightsSequencesA[$i] += $weightA; $weightsSequencesB[$i] += $weightB; + $weightShareSequenceB[$i] += $weightShareB; if ($minWeightsSequencesA[$i] > $weightA) { $minWeightsSequencesA[$i] = $weightA; } @@ -1108,21 +1110,21 @@ class APIController extends Controller for ($i = 0; $i < $minElectionCount; $i++) { $newWeight = new \stdClass(); - $newWeight->min_weight_a = $minWeightsSequencesA[$i]; - $newWeight->max_weight_a = $maxWeightsSequencesA[$i]; - $newWeight->min_weight_b = $minWeightsSequencesB[$i]; - $newWeight->max_weight_b = $maxWeightsSequencesB[$i]; + $newWeight->min_sum_weight_a = $minWeightsSequencesA[$i]; + $newWeight->max_sum_weight_a = $maxWeightsSequencesA[$i]; + $newWeight->min_sum_weight_b = $minWeightsSequencesB[$i]; + $newWeight->max_sum_weight_b = $maxWeightsSequencesB[$i]; //$newWeight->avg_voter_weight_a = $weightsSequencesA[$i] / $votersFactorA; //$newWeight->avg_voter_weight_b = $weightsSequencesB[$i] / $votersFactorB; - $newWeight->avg_weight_a = $weightsSequencesA[$i] / $templatePopulation->no_of_child_populations; - $newWeight->avg_weight_b = $weightsSequencesB[$i] / $templatePopulation->no_of_child_populations; - $newWeight->share_sum_weight_b = $weightShareSequenceB[$i]; - $newWeight->min_share_weight_b = $minWeightShareSequenceB[$i]; - $newWeight->max_share_weight_b = $maxWeightShareSequenceB[$i]; + $newWeight->avg_sum_weight_a = $weightsSequencesA[$i] / $templatePopulation->no_of_child_populations; + $newWeight->avg_sum_weight_b = $weightsSequencesB[$i] / $templatePopulation->no_of_child_populations; + $newWeight->share_sum_weight_b = $weightShareSequenceB[$i] / $templatePopulation->no_of_child_populations; + $newWeight->min_share_sum_weight_b = $minWeightShareSequenceB[$i]; + $newWeight->max_share_sum_weight_b = $maxWeightShareSequenceB[$i]; array_push($weightsAvgTimeline, $newWeight); } - $templatePopulation->weights_timeline = $weightsAvgTimeline; + $templatePopulation->weights = $weightsAvgTimeline; $endTime = microtime(true); $metadata->process_time = round($endTime - $startTime, 5); diff --git a/public/js/app.js b/public/js/app.js index 462f88f6855c6b394bc60519ff3e0a53f81ffb24..b6e20140dc8db7413db96a07511d3150a2255e35 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -3706,10 +3706,43 @@ __webpack_require__.r(__webpack_exports__); "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var vue_select__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue-select */ "./node_modules/vue-select/dist/vue-select.js"); -/* harmony import */ var vue_select__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(vue_select__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var vue_select_dist_vue_select_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue-select/dist/vue-select.css */ "./node_modules/vue-select/dist/vue-select.css"); -/* harmony import */ var vue_select_dist_vue_select_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(vue_select_dist_vue_select_css__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _charts_line_chart__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./charts/line-chart */ "./resources/js/components/charts/line-chart.vue"); +/* harmony import */ var vue_select__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue-select */ "./node_modules/vue-select/dist/vue-select.js"); +/* harmony import */ var vue_select__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(vue_select__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var vue_select_dist_vue_select_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue-select/dist/vue-select.css */ "./node_modules/vue-select/dist/vue-select.css"); +/* harmony import */ var vue_select_dist_vue_select_css__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(vue_select_dist_vue_select_css__WEBPACK_IMPORTED_MODULE_2__); +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// // // // @@ -3894,16 +3927,49 @@ __webpack_require__.r(__webpack_exports__); // // // +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + /* harmony default export */ __webpack_exports__["default"] = ({ name: "population-template-show", components: { - vSelect: vue_select__WEBPACK_IMPORTED_MODULE_0___default.a + LineChart: _charts_line_chart__WEBPACK_IMPORTED_MODULE_0__["default"], + vSelect: vue_select__WEBPACK_IMPORTED_MODULE_1___default.a }, data: function data() { return { - current_election_type_key: null, + current_election_timeline_key: null, + auto_fetch_weights_timeline: false, + analytics_weights_timeline: null, show_child_populations_details: true, show_child_populations_in_main: false, show_analytics_weights: false, @@ -3931,9 +3997,99 @@ __webpack_require__.r(__webpack_exports__); current_template: null, feedback: null, template_id: route().params.template_id, - template_name: null + template_name: null, + h_500_chart_styles: { + height: '500px', + width: '100%', + position: 'relative' + } }; }, + computed: { + weights_timeline_chart_data: function weights_timeline_chart_data() { + console.log('computing weights chart data'); + var labels = []; + var avg_sum_weight_a = []; + var min_sum_weight_a = []; + var max_sum_weight_a = []; + var avg_sum_weight_b = []; + var min_sum_weight_b = []; + var max_sum_weight_b = []; + var share_sum_weight_b = []; + var min_share_sum_weight_b = []; + var max_share_sum_weight_b = []; + this.analytics_weights_timeline.weights.forEach(function (value, idx) { + labels.push(idx); + avg_sum_weight_a.push(value.avg_sum_weight_a); + min_sum_weight_a.push(value.min_sum_weight_a); + max_sum_weight_a.push(value.max_sum_weight_a); + avg_sum_weight_b.push(value.avg_sum_weight_b); + min_sum_weight_b.push(value.min_sum_weight_b); + max_sum_weight_b.push(value.max_sum_weight_b); + share_sum_weight_b.push(value.share_sum_weight_b); + min_share_sum_weight_b.push(value.min_share_sum_weight_b); + max_share_sum_weight_b.push(value.max_share_sum_weight_b); + }); + return { + labels: labels, + datasets: [{ + label: 'Group A (avg sum Weight)', + borderColor: '#169c03', + fill: false, + data: avg_sum_weight_a, + yAxisID: 'left-y-axis' + }, { + label: 'Group A (min sum Weight)', + borderColor: '#819c67', + fill: false, + data: min_sum_weight_a, + yAxisID: 'left-y-axis' + }, { + label: 'Group A (max sum Weight)', + borderColor: '#819c67', + fill: false, + data: max_sum_weight_a, + yAxisID: 'left-y-axis' + }, { + label: 'Group B (avg sum Weight)', + borderColor: '#00259b', + fill: false, + data: avg_sum_weight_b, + yAxisID: 'left-y-axis' + }, { + label: 'Group B (min sum Weight)', + borderColor: '#517e9b', + fill: false, + data: min_sum_weight_b, + yAxisID: 'left-y-axis' + }, { + label: 'Group B (max sum Weight)', + borderColor: '#517e9b', + fill: false, + data: max_sum_weight_b, + yAxisID: 'left-y-axis' + }, { + label: 'Group B - sum Weight share', + borderColor: '#b73c33', + fill: false, + data: share_sum_weight_b, + yAxisID: 'right-y-axis' + }, { + label: 'Group B - min Weight share', + borderColor: '#b78365', + fill: false, + data: min_share_sum_weight_b, + yAxisID: 'right-y-axis' + }, { + label: 'Group B - max Weight share', + borderColor: '#b78365', + fill: false, + data: max_share_sum_weight_b, + yAxisID: 'right-y-axis' + }] + }; + } + }, mounted: function mounted() { this.fetchPopulationTemplate(); }, @@ -4005,6 +4161,18 @@ __webpack_require__.r(__webpack_exports__); _this3.updateChildPopulationStats(key); + console.log(_this3.auto_fetch_weights_timeline); + console.log(_this3.show_analytics_weights); + console.log(_this3.current_election_timeline_key); + console.log(population.election_type); + console.log(_this3.current_election_timeline_key == population.election_type); + + if (_this3.auto_fetch_weights_timeline && _this3.show_analytics_weights && _this3.current_election_timeline_key.value == population.election_type) { + console.log("OK"); + + _this3.fetchWeightsTimeline(); + } + _this3.running_elections_lock = false; })["catch"](function (err) { _this3.feedback = 'election error'; @@ -4030,6 +4198,22 @@ __webpack_require__.r(__webpack_exports__); })["catch"](function (err) { _this4.feedback = 'population stats fetching error'; }); + }, + fetchWeightsTimeline: function fetchWeightsTimeline() { + var _this5 = this; + + axios.get(route('internal.api.template.analytics.weights', { + "template": this.current_template.id + }), { + params: { + 'election_type': this.current_election_timeline_key.value + } + }).then(function (response) { + _this5.analytics_weights_timeline = response.data; + _this5.feedback = 'weights timeline fetched'; + })["catch"](function (err) { + _this5.feedback = 'weights timeline fetching error'; + }); } } }); @@ -81968,6 +82152,259 @@ var render = function() { ]) ]), _vm._v(" "), + _c("div", { staticClass: "row" }, [ + _c("div", { staticClass: "col-lg-12 col-md-12" }, [ + _vm.show_analytics_weights + ? _c("div", { staticClass: "card" }, [ + _c("div", { staticClass: "card-header" }, [ + _vm._v("Analytics. Elections timeline (weights)") + ]), + _vm._v(" "), + _c("div", { staticClass: "card-body" }, [ + _c("div", { staticClass: "row" }, [ + _c( + "div", + { staticClass: "col-md-4 col-lg-4" }, + [ + _c("v-select", { + attrs: { + options: _vm.election_type_selector, + id: "election_timeline_selector", + placeholder: "Choose election type", + label: "text" + }, + model: { + value: _vm.current_election_timeline_key, + callback: function($$v) { + _vm.current_election_timeline_key = $$v + }, + expression: "current_election_timeline_key" + } + }) + ], + 1 + ), + _vm._v(" "), + _c("div", { staticClass: "col-md-4 col-lg-4" }, [ + _c( + "button", + { + staticClass: "btn-xs", + class: { + "btn-primary": + _vm.current_election_timeline_key + }, + attrs: { + disabled: !_vm.current_election_timeline_key + }, + on: { + click: function($event) { + $event.preventDefault() + return _vm.fetchWeightsTimeline($event) + } + } + }, + [ + _vm.current_election_timeline_key + ? _c("i", [ + _vm._v( + "Fetch " + + _vm._s( + _vm.current_election_timeline_key + .text + ) + + " weights timeline" + ) + ]) + : _c("i", [_vm._v("Select election type")]) + ] + ) + ]), + _vm._v(" "), + _c("div", { staticClass: "col-md-4 col-lg-4" }, [ + _c("input", { + directives: [ + { + name: "model", + rawName: "v-model", + value: _vm.auto_fetch_weights_timeline, + expression: "auto_fetch_weights_timeline" + } + ], + attrs: { type: "checkbox" }, + domProps: { + checked: Array.isArray( + _vm.auto_fetch_weights_timeline + ) + ? _vm._i( + _vm.auto_fetch_weights_timeline, + null + ) > -1 + : _vm.auto_fetch_weights_timeline + }, + on: { + change: function($event) { + var $$a = _vm.auto_fetch_weights_timeline, + $$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.auto_fetch_weights_timeline = $$a.concat( + [$$v] + )) + } else { + $$i > -1 && + (_vm.auto_fetch_weights_timeline = $$a + .slice(0, $$i) + .concat($$a.slice($$i + 1))) + } + } else { + _vm.auto_fetch_weights_timeline = $$c + } + } + } + }), + _vm._v(" "), + _c("label", { staticClass: "text-info" }, [ + _vm._v( + "Auto update weights timeline after election" + ) + ]) + ]) + ]), + _vm._v(" "), + _c("hr"), + _vm._v(" "), + _vm.analytics_weights_timeline + ? _c("div", { staticClass: "row" }, [ + _c( + "div", + { staticClass: "col-md-12 col-lg-12" }, + [ + _c("h5", [ + _vm._v("Timeline for delegation weights") + ]), + _vm._v( + "\n Number of child populations: " + ), + _c("strong", [ + _vm._v( + _vm._s( + _vm.analytics_weights_timeline + .no_of_child_populations + ) + ) + ]), + _vm._v(" locked to election type - "), + _c("strong", [ + _vm._v( + _vm._s( + _vm.analytics_weights_timeline + .report_metadata.election_type + ) + ) + ]), + _vm._v( + ",\n " + ), + _c("br"), + _vm._v( + "\n Number of voters: " + ), + _c("strong", [ + _vm._v( + "(A:" + + _vm._s( + _vm.analytics_weights_timeline + .no_of_voters_a + ) + + ", B:" + + _vm._s( + _vm.analytics_weights_timeline + .no_of_voters_b + ) + + ")" + ) + ]), + _vm._v(" "), + _c("br"), + _vm._v( + "\n Number of elections: " + ), + _c("strong", [ + _vm._v( + _vm._s( + _vm.analytics_weights_timeline + .min_election_count + ) + ) + ]), + _vm._v(" "), + _c("i", [ + _vm._v( + "(maximum number in a single child population: " + + _vm._s( + _vm.analytics_weights_timeline + .max_election_count + ) + ) + ]), + _vm._v( + ")\n\n " + ), + _c("line-chart", { + attrs: { + "chart-data": + _vm.weights_timeline_chart_data, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [ + { + id: "left-y-axis", + type: "linear", + position: "left" + }, + { + id: "right-y-axis", + type: "linear", + position: "right", + ticks: { min: 0, max: 1 } + } + ] + } + }, + styles: _vm.h_500_chart_styles + } + }) + ], + 1 + ) + ]) + : !_vm.current_election_timeline_key + ? _c("div", [ + _c("i", [ + _vm._v( + "N/A use Election Timeline Selector to mark election type" + ) + ]) + ]) + : _c("div", [ + _c("i", [ + _vm._v( + 'N/A yet. Use "Fetch Timeline Weights" or mark "Auto Update" and run elections' + ) + ]) + ]) + ]) + ]) + : _vm._e() + ]) + ]), + _vm._v(" "), _c("div", { staticClass: "row" }, [ _c("div", { staticClass: "col-md-12 col-lg-12" }, [ _vm.show_child_populations_details diff --git a/resources/js/components/population-template-show.vue b/resources/js/components/population-template-show.vue index 0a5a4efa247ed1afa4d96928719a622b97be3d1f..fc43feeb0d7819b6c1946dbe685a2638ecc452d3 100644 --- a/resources/js/components/population-template-show.vue +++ b/resources/js/components/population-template-show.vue @@ -114,6 +114,67 @@ </div> </div> </div> + <div class="row"> + <div class="col-lg-12 col-md-12" > + <div class="card" v-if="show_analytics_weights"> + <div class="card-header">Analytics. Elections timeline (weights)</div> + <div class="card-body"> + <div class="row"> + <div class="col-md-4 col-lg-4"> + <v-select :options="election_type_selector" + id="election_timeline_selector" + v-model="current_election_timeline_key" + placeholder="Choose election type" + label="text"> + </v-select> + </div> + <div class="col-md-4 col-lg-4"> + <button :disabled="!current_election_timeline_key" + v-on:click.prevent="fetchWeightsTimeline" + v-bind:class="{'btn-primary' : current_election_timeline_key }" + class="btn-xs"> + <i v-if="current_election_timeline_key">Fetch {{current_election_timeline_key.text}} weights timeline</i> + <i v-else>Select election type</i> + </button> + </div> + <div class="col-md-4 col-lg-4"> + <input type="checkbox" v-model="auto_fetch_weights_timeline"> + <label class="text-info">Auto update weights timeline after election</label> + </div> + </div> + <hr> + <div class="row" v-if="analytics_weights_timeline" > + <div class="col-md-12 col-lg-12"> + <h5>Timeline for delegation weights</h5> + Number of child populations: <strong>{{analytics_weights_timeline.no_of_child_populations}}</strong> locked to election type - <strong>{{analytics_weights_timeline.report_metadata.election_type}}</strong>, + <br> + Number of voters: <strong>(A:{{analytics_weights_timeline.no_of_voters_a}}, B:{{analytics_weights_timeline.no_of_voters_b}})</strong> + <br> + Number of elections: <strong>{{analytics_weights_timeline.min_election_count}}</strong> + <i>(maximum number in a single child population: {{analytics_weights_timeline.max_election_count}}</i>) + + <line-chart :chart-data="weights_timeline_chart_data" + :options="{ + maintainAspectRatio: false, + scales: { + yAxes: [ + {id: 'left-y-axis',type: 'linear',position: 'left'}, + {id: 'right-y-axis',type: 'linear',position: 'right',ticks: {min: 0, max:1}} + ] + } + }" + :styles="h_500_chart_styles" + ></line-chart> + </div> + </div> + <div v-else-if="!current_election_timeline_key"> + <i>N/A use Election Timeline Selector to mark election type</i> + </div> + <div v-else><i>N/A yet. Use "Fetch Timeline Weights" or mark "Auto Update" and run elections</i></div> + </div> + </div> + </div> + </div> <div class="row"> <div class="col-md-12 col-lg-12"> <div class="card" v-if="show_child_populations_details"> @@ -184,15 +245,18 @@ <script> + import LineChart from "./charts/line-chart"; import vSelect from "vue-select"; import 'vue-select/dist/vue-select.css'; export default { name: "population-template-show", - components: {vSelect}, + components: {LineChart, vSelect}, data() { return { - current_election_type_key: null, + current_election_timeline_key: null, + auto_fetch_weights_timeline: false, + analytics_weights_timeline: null, show_child_populations_details: true, show_child_populations_in_main: false, show_analytics_weights: false, @@ -225,7 +289,108 @@ current_template: null, feedback: null, template_id: route().params.template_id, - template_name: null + template_name: null, + h_500_chart_styles: { + height: '500px', + width: '100%', + position: 'relative' + } + } + }, + computed: { + weights_timeline_chart_data() { + console.log('computing weights chart data'); + let labels = []; + let avg_sum_weight_a = []; + let min_sum_weight_a = []; + let max_sum_weight_a = []; + let avg_sum_weight_b = []; + let min_sum_weight_b = []; + let max_sum_weight_b = []; + let share_sum_weight_b = []; + let min_share_sum_weight_b = []; + let max_share_sum_weight_b = []; + this.analytics_weights_timeline.weights.forEach((value,idx) => { + labels.push(idx); + avg_sum_weight_a.push(value.avg_sum_weight_a); + min_sum_weight_a.push(value.min_sum_weight_a); + max_sum_weight_a.push(value.max_sum_weight_a); + avg_sum_weight_b.push(value.avg_sum_weight_b); + min_sum_weight_b.push(value.min_sum_weight_b); + max_sum_weight_b.push(value.max_sum_weight_b); + share_sum_weight_b.push(value.share_sum_weight_b); + min_share_sum_weight_b.push(value.min_share_sum_weight_b); + max_share_sum_weight_b.push(value.max_share_sum_weight_b); + }); + return { + labels: labels, + datasets: [ + { + label: 'Group A (avg sum Weight)', + borderColor: '#169c03', + fill: false, + data: avg_sum_weight_a, + yAxisID: 'left-y-axis' + }, + { + label: 'Group A (min sum Weight)', + borderColor: '#819c67', + fill: false, + data: min_sum_weight_a, + yAxisID: 'left-y-axis' + }, + { + label: 'Group A (max sum Weight)', + borderColor: '#819c67', + fill: false, + data: max_sum_weight_a, + yAxisID: 'left-y-axis' + }, + + { + label: 'Group B (avg sum Weight)', + borderColor: '#00259b', + fill: false, + data: avg_sum_weight_b, + yAxisID: 'left-y-axis' + }, + { + label: 'Group B (min sum Weight)', + borderColor: '#517e9b', + fill: false, + data: min_sum_weight_b, + yAxisID: 'left-y-axis' + }, + { + label: 'Group B (max sum Weight)', + borderColor: '#517e9b', + fill: false, + data: max_sum_weight_b, + yAxisID: 'left-y-axis' + }, + { + label: 'Group B - sum Weight share', + borderColor: '#b73c33', + fill: false, + data: share_sum_weight_b, + yAxisID: 'right-y-axis' + }, + { + label: 'Group B - min Weight share', + borderColor: '#b78365', + fill: false, + data: min_share_sum_weight_b, + yAxisID: 'right-y-axis' + }, + { + label: 'Group B - max Weight share', + borderColor: '#b78365', + fill: false, + data: max_share_sum_weight_b, + yAxisID: 'right-y-axis' + } + ] + } } }, mounted() { @@ -291,6 +456,15 @@ this.last_elections_data = response.data; this.last_elections_data.population_name = population.name; this.updateChildPopulationStats(key); + console.log(this.auto_fetch_weights_timeline); + console.log(this.show_analytics_weights); + console.log(this.current_election_timeline_key); + console.log(population.election_type) + console.log(this.current_election_timeline_key == population.election_type); + if(this.auto_fetch_weights_timeline && this.show_analytics_weights && this.current_election_timeline_key.value == population.election_type) { + console.log("OK"); + this.fetchWeightsTimeline(); + } this.running_elections_lock = false; }).catch((err) => { this.feedback = 'election error'; @@ -315,6 +489,24 @@ }).catch((err) => { this.feedback = 'population stats fetching error'; }); + }, + fetchWeightsTimeline() { + axios.get( + route( + 'internal.api.template.analytics.weights', + { + "template":this.current_template.id + } + ), { + params: { + 'election_type': this.current_election_timeline_key.value + } + }).then((response) => { + this.analytics_weights_timeline = response.data; + this.feedback = 'weights timeline fetched'; + }).catch((err) => { + this.feedback = 'weights timeline fetching error'; + }); } } }