jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/391490 )
Change subject: Simplify and fix breakdowns and other data
......................................................................
Simplify and fix breakdowns and other data
Three substantial refactorings of the data structures and related code:
First, there is always a breakdown, so we don't have to have duplicated
code for the breakdown vs. no-breakdown case. The no-breakdown case is
now just a breakdown with a single value of "total" that's inserted when
the GraphModel is created.
Second, the "activeBreakdown" is now explicitly set instead of being
inferred from the breakdown that has "on" set to true. This makes it
more natural to sync with the UI and actually inspires the UI to be a
little more intuitive in my opinion (this should be checked by others).
Third, the GraphModel constructor no longer takes data. The data is set
after the fact when the promise returns. This allows the Detail to
coordinate changes in breakdowns without re-rendering and deep-watching
the graphModel property.
I don't think this change cleans up the code completely, I'm still being
clumsy about what data's being passed around. But it does work in
Firefox and I think it simplifies the code.
Bug: T180556
Change-Id: I034b70b3d5a0034aa87013650884691c1c94e9f9
---
M src/apis/aqs.js
M src/apis/sitematrix.js
M src/components/RouterLink.vue
M src/components/dashboard/MetricBarWidget.vue
M src/components/dashboard/MetricLineWidget.vue
M src/components/dashboard/MetricListWidget.vue
M src/components/dashboard/MetricWidget.vue
M src/components/detail/Breakdowns.vue
M src/components/detail/Detail.vue
M src/components/detail/DetailSidebar.vue
M src/components/detail/GraphPanel.vue
M src/components/detail/SimpleLegend.vue
M src/components/detail/chart/BarChart.vue
M src/components/detail/chart/LineChart.vue
M src/components/detail/chart/TableChart.vue
M src/config/index.js
M src/config/metrics/content.js
M src/config/metrics/contributing.js
M src/config/metrics/metricSchema.js
M src/config/metrics/reading.js
M src/lodash-custom-bundle.js
M src/models/DimensionalData.js
M src/models/GraphModel.js
M src/router/index.js
M src/router/routes.js
M src/store/index.js
M src/utils.js
M test/DimensionalData.spec.js
M test/GraphModel.spec.js
M test/Router.spec.js
M test/components/LineChart.spec.js
31 files changed, 647 insertions(+), 738 deletions(-)
Approvals:
Fdans: Looks good to me, approved
jenkins-bot: Verified
diff --git a/src/apis/aqs.js b/src/apis/aqs.js
index e4f6213..a7261ff 100644
--- a/src/apis/aqs.js
+++ b/src/apis/aqs.js
@@ -55,6 +55,8 @@
const key = _.trim(k, '{}');
url = url.replace(k, p[key]);
});
+
+ // console.info('getting ', url.replace(/https.*metrics\//,
''));
return new Promise((resolve, reject) => {
$.get({
url: url,
diff --git a/src/apis/sitematrix.js b/src/apis/sitematrix.js
index 85df7cb..cb41110 100644
--- a/src/apis/sitematrix.js
+++ b/src/apis/sitematrix.js
@@ -23,8 +23,8 @@
let lookup = matrix.then(function (data) {
let result = {
- 'all': 'all-projects',
- 'all-projects':'all-projects'
+ 'all': config.ALL_PROJECTS,
+ 'all-projects': config.ALL_PROJECTS,
};
_.forEach(data.sitematrix, function (languageGroup) {
@@ -132,7 +132,7 @@
family: 'all', title: 'All Project Families', projects: [{
title: 'All Languages',
description: 'Aggregate of all project families',
- code: 'all-projects',
+ code: config.ALL_PROJECTS,
dbname: 'all',
}],
}
diff --git a/src/components/RouterLink.vue b/src/components/RouterLink.vue
index a3e1b50..f581ec3 100644
--- a/src/components/RouterLink.vue
+++ b/src/components/RouterLink.vue
@@ -15,7 +15,7 @@
methods: {
commitState () {
if (!this.isCurrent()) {
- this.$store.commit('resetState', this.to);
+ this.$store.commit('resetNavigationState', this.to);
}
},
highlightClass () {
diff --git a/src/components/dashboard/MetricBarWidget.vue
b/src/components/dashboard/MetricBarWidget.vue
index b2dfc2e..6444998 100644
--- a/src/components/dashboard/MetricBarWidget.vue
+++ b/src/components/dashboard/MetricBarWidget.vue
@@ -13,12 +13,11 @@
import ArrowIcon from '../ArrowIcon'
import * as d3 from 'd3-selection'
import * as scales from 'd3-scale'
-import * as arr from 'd3-array'
import config from '../../config'
export default {
name: 'metric-bar-widget',
- props: ['metricData', 'graphModel'],
+ props: ['graphModel', 'data'],
components: {
ArrowIcon,
@@ -29,17 +28,15 @@
},
watch: {
- graphModel: {
- handler: function () {
- this.drawChart();
- },
- deep: true
- }
+ data: function () {
+ this.drawChart();
+ },
},
methods: {
drawChart () {
+
const self = this;
const root = d3.select(this.$el).select('.bar-chart'),
@@ -52,23 +49,21 @@
);
g.selectAll('*').remove();
- const rowData = this.graphModel.getGraphData();
-
const n = root.node(),
width = n.offsetWidth - margin.left - margin.right,
height = n.offsetHeight - margin.top - margin.bottom -
padding,
x = scales.scaleBand().rangeRound([0, width]).padding(0.3),
y = scales.scaleLinear().rangeRound([height, 0]);
- const min = Math.min(0, arr.min(rowData.map((d) => d.total)));
+ const { min, max } = this.graphModel.getMinMax();
- x.domain(rowData.map((d) => d.month));
- y.domain([min, arr.max(rowData.map((d) => d.total))]);
+ x.domain(this.data.map((d) => d.month));
+ y.domain([min, max]);
svg.attr('width', n.offsetWidth).attr('height', n.offsetHeight);
g.attr('width', width).attr('height', height);
- const lastMonth = rowData[rowData.length - 1].month;
- g.append('g').selectAll('.bar').data(rowData)
+ const lastMonth = this.data[this.data.length - 1].month;
+ g.append('g').selectAll('.bar').data(this.data)
.enter().append('rect')
.attr('x', (d) => x(d.month))
.attr('y', (d) => {
@@ -84,7 +79,7 @@
})
.attr('fill', (d) =>
d.month === lastMonth ?
- self.metricData.darkColor :
self.metricData.lightColor
+ self.graphModel.config.darkColor :
self.graphModel.config.lightColor
);
if (min < 0) {
g.append('line')
@@ -92,7 +87,7 @@
.attr('x2', width)
.attr('y1', y(0))
.attr('y2', y(0))
- .style('stroke', self.metricData.lightColor)
+ .style('stroke', self.graphModel.config.lightColor)
.style('stroke-width', 0.5);
}
@@ -100,7 +95,7 @@
.attr('transform', `translate(${
x.bandwidth() / 2 - 3
},${12})`)
- .selectAll('.month').data(rowData)
+ .selectAll('.month').data(this.data)
.enter().append('text')
.attr('x', (d) => x(d.month))
.attr('y', height)
diff --git a/src/components/dashboard/MetricLineWidget.vue
b/src/components/dashboard/MetricLineWidget.vue
index be6f956..2c9bca7 100644
--- a/src/components/dashboard/MetricLineWidget.vue
+++ b/src/components/dashboard/MetricLineWidget.vue
@@ -18,7 +18,7 @@
export default {
name: 'metric-line-widget',
- props: ['metricData', 'graphModel'],
+ props: ['graphModel', 'data'],
components: {
ArrowIcon,
@@ -28,14 +28,15 @@
this.drawChart();
},
- updated () {
- this.drawChart();
+ watch: {
+ data: function () {
+ this.drawChart();
+ },
},
methods: {
drawChart () {
- const self = this;
const root = d3.select(this.$el).select('.line-chart'),
margin = {top: 0, right: 0, bottom: 0, left: 0},
@@ -47,55 +48,50 @@
);
g.selectAll('*').remove();
- const rowData = this.graphModel.getGraphData();
+ g.html("");
+ const n = root.node(),
+ width = n.offsetWidth - margin.left - margin.right,
+ height = n.offsetHeight - margin.top - margin.bottom -
padding,
+ min = Math.min(0, arr.min(this.data.map((d) => d.total))),
+ x = scales.scaleTime().rangeRound([0, width]),
+ y = scales.scaleLinear().rangeRound([height, min]);
- function resize () {
- g.html("");
- const n = root.node(),
- width = n.offsetWidth - margin.left - margin.right,
- height = n.offsetHeight - margin.top - margin.bottom -
padding,
- min = Math.min(0, arr.min(rowData.map((d) => d.total))),
- x = scales.scaleTime().rangeRound([0, width]),
- y = scales.scaleLinear().rangeRound([height, min]);
+ x.domain(arr.extent(this.data.map((d) => d.month)));
+ y.domain([0, arr.max(this.data.map((d) => d.total))]);
- x.domain(arr.extent(rowData.map((d) => d.month)));
- y.domain([0, arr.max(rowData.map((d) => d.total))]);
+ const area = shape.area()
+ .x((d) => x(d.month))
+ .y1((d) => y(d.total))
+ .y0(height)
- const area = shape.area()
- .x((d) => x(d.month))
- .y1((d) => y(d.total))
- .y0(height)
+ svg.attr('width', n.offsetWidth).attr('height', n.offsetHeight);
+ g.attr('width', width).attr('height', height);
+ g.append('path').datum(this.data)
+ .attr('d', area)
+ .style('fill', 'url(#grad-'+this.graphModel.config.area+')')
+ .style('stroke-width', '0');
+ let gradient = g.append('linearGradient')
+ .attr('id', 'grad-'+this.graphModel.config.area)
+ .attr('x1',"0%")
+ .attr('y1',"0%")
+ .attr('x2',"0%")
+ .attr('y2',"100%")
+ gradient.append('stop')
+ .attr('offset', '10%')
+ .attr('stop-color', this.graphModel.config.lightColor);
+ gradient.append('stop')
+ .attr('offset', '100%')
+ .attr('stop-color', this.graphModel.config.lightColor)
+ .attr('stop-opacity', 0);
- svg.attr('width', n.offsetWidth).attr('height',
n.offsetHeight);
- g.attr('width', width).attr('height', height);
- g.append('path').datum(rowData)
- .attr('d', area)
- .style('fill', 'url(#grad-'+self.graphModel.getArea()+')')
- .style('stroke-width', '0');
- let gradient = g.append('linearGradient')
- .attr('id', 'grad-'+self.graphModel.getArea())
- .attr('x1',"0%")
- .attr('y1',"0%")
- .attr('x2',"0%")
- .attr('y2',"100%")
- gradient.append('stop')
- .attr('offset', '10%')
- .attr('stop-color', self.metricData.lightColor);
- gradient.append('stop')
- .attr('offset', '100%')
- .attr('stop-color', self.metricData.lightColor)
- .attr('stop-opacity', 0);
-
- const line = shape.area()
- .x((d) => x(d.month))
- .y((d) => y(d.total));
- g.append('path').datum(rowData)
- .attr('d', line)
- .style('fill', 'none')
- .style('stroke-width', '2')
- .style('stroke', self.metricData.darkColor);
- }
- resize();
+ const line = shape.area()
+ .x((d) => x(d.month))
+ .y((d) => y(d.total));
+ g.append('path').datum(this.data)
+ .attr('d', line)
+ .style('fill', 'none')
+ .style('stroke-width', '2')
+ .style('stroke', this.graphModel.config.darkColor);
}
}
}
diff --git a/src/components/dashboard/MetricListWidget.vue
b/src/components/dashboard/MetricListWidget.vue
index bd0674f..ecb2452 100644
--- a/src/components/dashboard/MetricListWidget.vue
+++ b/src/components/dashboard/MetricListWidget.vue
@@ -1,14 +1,14 @@
<template>
-<div v-if="graphModel">
+<div>
<div class="ui medium statistic">
- <div class="label">{{metricData.fullName}}</div>
+ <div class="label">{{graphModel.config.fullName}}</div>
</div>
<div class="subdued">
- {{metricData.subtitle + ' for ' + currentMonth}}
+ {{graphModel.config.subtitle + ' for ' + currentMonth}}
</div>
<table class="widget list">
<tr v-for="(item, i) in sortedList">
- <td class="number">{{item.views | kmb}}</td>
+ <td class="number">{{item.views.total | kmb}}</td>
<td class="label">
<a v-on:click.stop target="_blank" :href="'\/\/' +
$store.state.project + '/wiki/' + item.article">
@@ -26,14 +26,14 @@
export default {
name: 'metric-list-widget',
- props: ['metricData', 'graphModel'],
+ props: ['graphModel', 'data'],
computed: {
currentMonth () {
return config.months[new Date().getMonth() + 1];
},
sortedList () {
- return this.graphModel.topXByY(this.metricData.key,
this.metricData.value).slice(0,4);
+ return this.data.slice(0, 4);
}
}
};
diff --git a/src/components/dashboard/MetricWidget.vue
b/src/components/dashboard/MetricWidget.vue
index aa93c58..7e3e986 100644
--- a/src/components/dashboard/MetricWidget.vue
+++ b/src/components/dashboard/MetricWidget.vue
@@ -6,13 +6,13 @@
</metric-placeholder-widget>
<div v-else>
<metric-list-widget
- v-if="metricData.type === 'list'"
- :metricData="metricData"
+ v-if="graphModel.config.type === 'list'"
+ :data="graphData"
:graphModel="graphModel">
</metric-list-widget>
<div v-else>
<div class="ui medium statistic">
- <div class="label">{{metricData.fullName}}</div>
+ <div class="label">{{graphModel.config.fullName}}</div>
<div
class="value">{{graphModel.formatNumberForMetric(lastMonth.total)}}</div>
</div>
<div>
@@ -23,13 +23,13 @@
</span>
</div>
<metric-bar-widget
- v-if="metricData.type === 'bars'"
- :metricData="metricData"
+ v-if="graphModel.config.type === 'bars'"
+ :data="graphData"
:graphModel="graphModel">
</metric-bar-widget>
<metric-line-widget
- v-else-if="metricData.type === 'lines'"
- :metricData="metricData"
+ v-else-if="graphModel.config.type === 'lines'"
+ :data="graphData"
:graphModel="graphModel">
</metric-line-widget>
<div class="ui horizontal small statistic">
@@ -52,6 +52,7 @@
</template>
<script>
+import Vue from 'vue';
import { mapState } from 'vuex';
import MetricBarWidget from './MetricBarWidget'
@@ -74,9 +75,8 @@
props: ['metric', 'area'],
data () {
return {
- metricData: undefined,
- graphModel: undefined,
- overlayMessage: null
+ graphModel: null,
+ overlayMessage: null,
}
},
@@ -90,43 +90,39 @@
RouterLink
},
- mounted () {
- this.loadConfig();
- },
-
- methods: {
- loadConfig () {
- this.metricData = config.metricData(this.metric.name, this.area);
- },
- getMonthValue (date) {
- return config.months[date.getMonth() + 1];
- }
- },
-
computed: Object.assign(
mapState([
'project'
]), {
- aqsParameters () {
- if (!this.metricData || !this.project) { return; }
- const defaults = this.metricData.defaults;
- const range = TimeRangeSelector.getDefaultTimeRange();
+ params () {
return {
- unique: Object.assign(
- defaults.unique,
- { project: [this.project] },
- ),
- common: Object.assign(
- defaults.common,
- {
- start: range[0],
- end: range[1],
- granularity: 'monthly'
- }
- )
+ project: this.project,
+ area: this.area,
+ metric: this.metric.name,
+ metricConfig: config.metricData(this.metric.name),
+ range: TimeRangeSelector.getDefaultTimeRange(),
+ granularity: 'monthly',
};
},
+
+ graphData () {
+ if (!this.graphModel) { return []; }
+
+ if (this.graphModel.config.type === 'list') {
+ return this.graphModel.graphData;
+ } else {
+ // normalize the data to look like the old data
+ // if building breakdowns into the widgets, look here first
+ return this.graphModel.graphData.map(d => ({
+ month: d.month,
+ total: d.total.total,
+ }));
+ }
+ },
+
monthOneYearAgo: function () {
+ if (!this.lastMonth) { return null; }
+
return this.graphData[_.indexOf(this.graphData,
this.lastMonth) - 12];
},
lastYearAggregation: function () {
@@ -135,10 +131,9 @@
lastMonth: function () {
return _.last(this.graphData);
},
- graphData: function () {
- return this.graphModel.getGraphData();
- },
changeMoM: function () {
+ if (!this.lastMonth) { return null; }
+
const data = this.graphData;
const prev = data[data.length - 2];
const diff = this.lastMonth.total - prev.total;
@@ -151,7 +146,7 @@
return ((diff / this.monthOneYearAgo.total) * 100).toFixed(2);
},
disabled: function () {
- return !this.metricData.global && this.$store.state.project
=== 'all-projects';
+ return !this.params.metricConfig.global &&
this.$store.state.project === config.ALL_PROJECTS;
},
aggregationType: function () {
return this.graphModel.getAggregateLabel();
@@ -159,24 +154,64 @@
}
),
+ mounted () {
+ this.aqsApi = new AQS();
+ this.loadData();
+ },
+
watch: {
- aqsParameters () {
- this.loadConfig();
+ params () {
+ this.graphModel = null;
+ // allow the placeholders to reset before loading new data
+ Vue.nextTick(() => this.loadData());
+ },
+ },
+
+ methods: {
+ getMonthValue (date) {
+ return config.months[date.getMonth() + 1];
+ },
+
+ loadData () {
+ const params = this.params;
+
if (this.disabled) {
- this.overlayMessage =
StatusOverlay.NON_GLOBAL(this.metricData.fullName);
- this.graphModel = null;
+ this.overlayMessage =
StatusOverlay.NON_GLOBAL(params.metricConfig.fullName);
return;
}
- const { unique, common } = this.aqsParameters;
- let dataPromise = aqsApi.getData(unique, common);
+
+ const defaults = params.metricConfig.defaults || {
+ unique: {},
+ common: {}
+ };
+ let uniqueParameters = Object.assign(
+ {},
+ defaults.unique,
+ {
+ project: [params.project]
+ },
+ );
+ const commonParameters = Object.assign(
+ {},
+ defaults.common,
+ {
+ start: params.range[0],
+ end: params.range[1],
+ granularity: params.granularity
+ }
+ );
+
+ let dataPromise = this.aqsApi.getData(uniqueParameters,
commonParameters);
this.overlayMessage = StatusOverlay.LOADING;
- dataPromise.catch(req => {
+
+ dataPromise.catch((req, status, error) => {
this.overlayMessage =
StatusOverlay.getMessageForStatus(req.status);
});
dataPromise.then(dimensionalData => {
this.overlayMessage = null;
- this.graphModel = new GraphModel(this.metricData,
dimensionalData);
- })
+ this.graphModel = new GraphModel(params.metricConfig);
+ this.graphModel.setData(dimensionalData);
+ });
},
},
};
diff --git a/src/components/detail/Breakdowns.vue
b/src/components/detail/Breakdowns.vue
index 8ee6906..3765dd6 100644
--- a/src/components/detail/Breakdowns.vue
+++ b/src/components/detail/Breakdowns.vue
@@ -4,54 +4,49 @@
<h3 class="header">Filter and Split</h3>
- <div v-for="b, i in graphModel.getBreakdowns()" class="breakdown">
+ <div v-for="b, i in graphModel.breakdowns" class="breakdown">
<div class="ui toggle checkbox">
<input
- type="checkbox"
+ type="radio"
:id="'breakdown' + b.breakdownName"
- v-model="b.on"
- @click="breakdownToggled(i)"
- :checked="shouldBeChecked(i)">
+ v-model="graphModel.activeBreakdown"
+ :value="b">
<label :for="'breakdown' + b.breakdownName">
- Split by <strong>{{b.name}}</strong>
- <i class="help circle icon" title="Split the total into parts
to see more detail. Filter to the parts you're interested in using the
checkboxes."/>
+ <span v-if="b.total">
+ Overall <strong>{{b.name}}</strong>
+ <i class="help circle icon" title="See the overall total"/>
+ </span>
+ <span v-else>
+ Split by <strong>{{b.name}}</strong>
+ <i class="help circle icon" title="Split the total into
parts to see more detail. Filter to the parts you're interested in using the
checkboxes."/>
+ </span>
</label>
</div>
- <label class="xui checkbox" v-for="bv in b.values">
- <input type="checkbox" v-model="bv.on" :disabled="!b.on"/>
- {{bv.name}}
- </label>
+ <div v-if="!b.total && isActive(b)">
+ <label class="xui checkbox" :class="{active: isActive(b)}"
+ v-for="bv in b.values">
+
+ <input type="checkbox" v-model="bv.on"
:disabled="!isActive(b)"/>
+ {{bv.name}}
+ </label>
+ </div>
</div>
</div>
</template>
<script>
+ import utils from '../../utils';
+
export default {
name: 'breakdowns',
props: ['graphModel'],
+
methods: {
- breakdownToggled (index) {
- this.graphModel.getBreakdowns().forEach((b, i) => {
- if(i != index) {
- this.graphModel.getBreakdowns()[i].on = false;
- }
- })
- this.updateState();
- },
- shouldBeChecked (index) {
- // HORRIBLE, this shouldn't have side effects
- if(!this.graphModel.getBreakdowns()[index].values.some(b =>
b.on)) {
- this.graphModel.getBreakdowns()[index].values.forEach(v =>
{
- v.on = true;
- })
- this.graphModel.getBreakdowns()[index].on = false;
- }
- },
- updateState () {
- this.$store.state.breakdowns =
JSON.parse(JSON.stringify(this.graphModel.getBreakdowns()));
+ isActive (b) {
+ return b === this.graphModel.activeBreakdown;
}
- }
+ },
}
</script>
diff --git a/src/components/detail/Detail.vue b/src/components/detail/Detail.vue
index 2ca9441..c52f858 100644
--- a/src/components/detail/Detail.vue
+++ b/src/components/detail/Detail.vue
@@ -3,31 +3,24 @@
<detail-sidebar
v-if="!fullscreen"
:otherMetrics="otherMetrics"
- :metric="metric"
- :area="area"
:graphModel="graphModel"
/>
<graph-panel
- :metricData="metricData"
- :granularity="granularity"
+ ref="graphPanel"
:graphModel="graphModel"
- :fullscreen="fullscreen"
:overlayMessage="overlayMessage"
@changeTimeRange="setTimeRange"
+ :fullscreen="fullscreen"
@toggleFullscreen="toggleFullscreen"
/>
-
- <metrics-modal
- :areasWithMetrics="areasWithMetrics"
- :highlightMetric="highlightMetric"
- @changeMetric="goHighlight">
-
- </metrics-modal>
</section>
</template>
<script>
+
+import Vue from 'vue';
+import { mapState, mapGetters } from 'vuex';
import StatusOverlay from '../StatusOverlay';
import MetricsModal from './MetricsModal';
@@ -35,10 +28,7 @@
import DetailSidebar from './DetailSidebar';
import TimeRangeSelector from '../TimeRangeSelector';
-import { mapState } from 'vuex';
-
import config from '../../config';
-import DimensionalData from '../../models/DimensionalData';
import GraphModel from '../../models/GraphModel';
import AQS from '../../apis/aqs';
@@ -55,9 +45,10 @@
},
data () {
return {
+ graphModel: null,
+
fullscreen: false,
areasWithMetrics: config.areasWithMetrics,
- highlightMetric: {},
defaultMetrics: {
contributing: 'active-editors',
@@ -67,17 +58,8 @@
otherMetrics: [],
- metricData: {},
-
- breakdowns: [],
-
- graphModel: null,
-
- project: 'all-projects',
- wiki: null,
overlayMessage: null,
range: TimeRangeSelector.getDefaultTimeRange(),
- granularity: 'monthly'
};
},
@@ -85,95 +67,116 @@
mapState([
'area',
'metric',
+ 'project',
]), {
- breakdown () {
- return (this.breakdowns || []).find((m) => m.on);
+ metricParameters () {
+ return {
+ project: this.project,
+ area: this.area,
+ metric: this.metric,
+ metricConfig: config.metricData(this.metric),
+ };
+ },
+
+ dataParameters () {
+ return {
+ range: this.range,
+ granularity: this.range[1] - this.range[0] < 100000 ?
'daily' : 'monthly',
+
+ breakdown: this.graphModel ?
this.graphModel.activeBreakdown : null,
+ };
},
}
),
watch: {
- '$store.getters.mainState': function () {
- this.wiki = this.$store.state.project;
- this.loadData();
- },
- 'range': function () {
- this.loadData();
- },
- '$store.state.breakdowns': {
- handler: function () {
- this.loadData();
- },
- deep: true
- },
- 'overlayMessage': function () {
+ overlayMessage () {
// when we display an error or loading in full-screen, the overlay
doesn't show and causes a broken interface
if (this.overlayMessage && this.overlayMessage !==
StatusOverlay.LOADING) { this.fullscreen = false; }
+ },
+
+ metricParameters () {
+ this.buildGraphModel();
+ },
+
+ dataParameters: {
+ handler () {
+ this.loadData();
+ },
+ deep: true,
},
},
mounted () {
- this.wiki = this.$store.state.project;
$('body').scrollTop(0);
- $('.ui.metrics.modal').modal();
- this.loadData();
+ this.aqsApi = new AQS();
+ Vue.nextTick(() => this.buildGraphModel());
},
methods: {
+ buildGraphModel () {
+ const params = this.metricParameters;
+
+ this.graphModel = new GraphModel(params.metricConfig);
+
+ this.otherMetrics = Object.keys(config.metrics)
+ .filter((m) => config.metrics[m].area === params.area)
+ .map((m) => Object.assign(config.metrics[m], { name: m }));
+ },
+
loadData () {
- if (!this.$store.state.project) { return; }
+ const params = Object.assign({}, this.metricParameters,
this.dataParameters);
- this.highlightMetric = { name: this.metric, area: this.area };
+ if (!params.metricConfig.global && params.project ===
config.ALL_PROJECTS) {
+ this.overlayMessage =
StatusOverlay.NON_GLOBAL(params.metricConfig.fullName);
- const metricData = Object.assign(config.metricData(this.metric,
this.area), {});
- this.metricData = metricData;
- if (!metricData.global && this.$store.state.project ===
'all-projects') {
- this.overlayMessage =
StatusOverlay.NON_GLOBAL(this.metricData.fullName);
} else {
- let aqsApi = new AQS();
- const defaults = this.metricData.defaults || {
+ const defaults = params.metricConfig.defaults || {
unique: {},
common: {}
};
let uniqueParameters = Object.assign(
+ {},
defaults.unique,
{
- project: [this.$store.state.project]
+ project: [params.project]
},
);
- const activeBreakdown = this.graphModel &&
this.graphModel.getActiveBreakdown();
- if (activeBreakdown) {
- const breakdownKeys = activeBreakdown.values.reduce((p, c)
=> {
- c.on && p.push(c.key);
- return p;
- }, []);
- uniqueParameters[activeBreakdown.breakdownName] =
breakdownKeys;
- }
- let dataPromise = aqsApi.getData(uniqueParameters,
- Object.assign(
- defaults.common,
- {
- start: this.range[0],
- end: this.range[1],
- granularity: this.granularity
- }
- )
+ const commonParameters = Object.assign(
+ {},
+ defaults.common,
+ {
+ start: params.range[0],
+ end: params.range[1],
+ granularity: params.granularity
+ }
);
+
+ if (params.breakdown && !params.breakdown.total) {
+ let breakdownKeys = params.breakdown.values.filter(bv =>
bv.on).map(bv => bv.key);
+
+ // in this case, the user de-selected the last value,
toggle back to Total
+ if (!breakdownKeys.length) {
+ // also re-select everything otherwise this will loop
+ // to see what I mean, try deleting the next line and
de-selecting all values
+ params.breakdown.values.forEach(bv => bv.on = true);
+ this.graphModel.activeBreakdown =
this.graphModel.breakdowns[0];
+ return;
+ }
+ uniqueParameters[params.breakdown.breakdownName] =
breakdownKeys;
+ }
+
+ let dataPromise = this.aqsApi.getData(uniqueParameters,
commonParameters);
this.overlayMessage = StatusOverlay.LOADING;
- const prevBreakdowns = this.graphModel &&
this.graphModel.getBreakdowns();
+
dataPromise.catch((req, status, error) => {
this.overlayMessage =
StatusOverlay.getMessageForStatus(req.status);
});
dataPromise.then(dimensionalData => {
this.overlayMessage = null;
- this.graphModel = new GraphModel(metricData,
dimensionalData, prevBreakdowns);
+ this.graphModel.setData(dimensionalData);
});
}
- const metrics = config.metrics;
- const relevantMetrics = Object.keys(metrics)
- .filter((m) => metrics[m].area === this.area );
- this.otherMetrics =
- relevantMetrics.map((m) => Object.assign(metrics[m], { name: m
}));
},
changeChart (t) {
@@ -183,35 +186,10 @@
toggleFullscreen () {
this.fullscreen = !this.fullscreen;
-
- // TODO: hack, figure out a way to re-render bar without this
- const t = this.metricData,
- self = this;
- this.metricData = {};
- setTimeout(function () {
- self.metricData = t;
- }, 0);
- },
-
- addAnotherWiki () {
- $('.add.wiki.design').toggle('highlight');
- },
-
- changeHighlight (name, area) {
- this.highlightMetric = { name, area };
- },
-
- goHighlight (name, area) {
- this.changeHighlight(name, area);
- $('.ui.metrics.modal').modal('hide');
+ Vue.nextTick(() => this.$refs.graphPanel.redrawGraph());
},
setTimeRange (newRange) {
- if (newRange[1] - newRange[0] < 100000) {
- this.granularity = 'daily';
- } else {
- this.granularity = 'monthly';
- }
this.range = newRange;
}
},
diff --git a/src/components/detail/DetailSidebar.vue
b/src/components/detail/DetailSidebar.vue
index 14feec0..03a9d04 100644
--- a/src/components/detail/DetailSidebar.vue
+++ b/src/components/detail/DetailSidebar.vue
@@ -10,7 +10,7 @@
<h3 class="header">Metrics</h3>
<router-link v-for="o in otherMetrics" :key="o.name"
- :to="{project: $store.state.project, area, metric: o.name}"
+ :to="{project, area, metric: o.name}"
class="ui line label">
{{o.fullName}}
</router-link>
@@ -24,6 +24,8 @@
</template>
<script>
+import { mapState } from 'vuex';
+
import WikiSelector from '../WikiSelector';
import Breakdowns from './Breakdowns';
import sitematrix from '../../apis/sitematrix';
@@ -34,7 +36,7 @@
export default {
name: 'detail-sidebar',
- props: ['otherMetrics','metric','graphModel','area'],
+ props: ['otherMetrics', 'graphModel'],
data () {
return {
wiki: {
@@ -47,6 +49,10 @@
Breakdowns,
RouterLink,
},
+ computed: mapState([
+ 'project',
+ 'area',
+ ]),
methods: {
viewMoreMetrics () {
$('.ui.metrics.modal', this.$el).modal('show');
diff --git a/src/components/detail/GraphPanel.vue
b/src/components/detail/GraphPanel.vue
index fb8ffae..a90a810 100644
--- a/src/components/detail/GraphPanel.vue
+++ b/src/components/detail/GraphPanel.vue
@@ -1,16 +1,20 @@
<template>
<section class="graph panel">
- <div class="ui clearing basic segment">
- <div v-if="graphModel && !overlayMessage">
+ <div class="ui clearing basic segment" v-if="graphModel">
+ <div>
<h2 class="ui left floated header">
- <a class='metric link' :href="metricData.info_url"
target="_blank">
- {{metricData.fullName || 'No data yet... '}}
+ <a class='metric link' :href="graphModel.config.info_url"
target="_blank">
+ {{graphModel.config.fullName || 'No data yet... '}}
</a>
<span class="subdued granularity">{{granularity}}</span>
</h2>
<div class="ui right floated basic fudge segment">
- <simple-legend v-if="activeBreakdown && chartComponent !==
'table-chart'" class="simple legend"
:breakdown="activeBreakdown"></simple-legend>
+ <simple-legend
+ v-if="activeBreakdown && chartComponent !== 'table-chart'"
+ class="simple legend"
+ :breakdown="activeBreakdown">
+ </simple-legend>
<div class="ui right floated icon buttons">
<button @click="download" class="ui icon button"
title="Download">
@@ -34,20 +38,20 @@
</div>
</div>
<component
+ ref="graph"
v-if="graphModel"
:is="chartComponent"
:graphModel="graphModel"
- :metricData="metricData"
- :graphData="graphModel.getGraphData()">
+ :data="graphModel.graphData">
</component>
- <div class="ui center aligned basic segment" v-if="graphModel &&
metricData.type !== 'list'">
+ <div class="ui center aligned basic segment"
v-if="graphModel.config.type !== 'list'">
<h5>
{{graphModel.getAggregateLabel()}}:
- {{graphModel.formatNumberForMetric(aggregate)}}
{{metricData.fullName}}
+ {{graphModel.formatNumberForMetric(aggregate)}}
{{graphModel.config.fullName}}
<arrow-icon :value="changeOverRange"></arrow-icon>
{{changeOverRange}}% over this time range.
</h5>
- <p>{{metricData.description}}. <a class='metric link'
:href="metricData.info_url" target="_blank">More info about this metric.</a></p>
+ <p>{{graphModel.config.description}}. <a class='metric link'
:href="graphModel.config.info_url" target="_blank">More info about this
metric.</a></p>
</div>
</div>
<div class="ui center aligned subdued basic segment">
@@ -87,7 +91,7 @@
EmptyChart,
StatusOverlay
},
- props: ['metricData', 'fullscreen', 'graphModel', 'overlayMessage',
'granularity'],
+ props: ['fullscreen', 'graphModel', 'overlayMessage', 'granularity'],
computed: {
chartTypes: function () {
return this.getChartTypes();
@@ -108,8 +112,8 @@
return ((data[data.length - 1] - data[0]) / data[0] *
100).toFixed(2);
},
activeBreakdown: function () {
- return this.graphModel.getActiveBreakdown();
- }
+ return this.graphModel.activeBreakdown;
+ },
},
data () {
return {
@@ -122,7 +126,14 @@
]
}
},
+
methods: {
+ // PUBLIC: used by parent components
+ redrawGraph () {
+ if (this.$refs.graph && this.$refs.graph.redraw) {
+ this.$refs.graph.redraw();
+ }
+ },
changeChart (t) {
this.chartType = t.chart;
},
@@ -131,20 +142,20 @@
},
getChartTypes () {
return this.availableChartTypes.filter((c) => {
- if (!this.metricData) { return false; }
+ if (!this.graphModel) { return false; }
if (c.chart === 'table') return true;
- if (this.metricData.type === 'bars') { return c.chart !==
'line'; }
- if (this.metricData.type === 'lines') { return c.chart ===
'line'; }
+ if (this.graphModel.config.type === 'bars') { return c.chart
!== 'line'; }
+ if (this.graphModel.config.type === 'lines') { return c.chart
=== 'line'; }
});
},
toggleFullscreen () {
this.$emit('toggleFullscreen');
},
download () {
- const data = this.graphModel.getGraphData();
+ const data = this.graphModel.graphData;
let a = window.document.createElement('a');
a.href = window.URL.createObjectURL(new
Blob([JSON.stringify(data)], {type: 'text/json'}));
- a.download = this.metricData.name + '.json';
+ a.download = this.graphModel.config.name + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
diff --git a/src/components/detail/SimpleLegend.vue
b/src/components/detail/SimpleLegend.vue
index 089e600..f49fe56 100644
--- a/src/components/detail/SimpleLegend.vue
+++ b/src/components/detail/SimpleLegend.vue
@@ -18,7 +18,7 @@
props: ['breakdown'],
methods: {
getColor (key) {
- return config.getColorForBreakdown(this.breakdown, key);
+ return config.getColorForBreakdown(this.breakdown, key,
this.$store.state.area);
}
}
}
diff --git a/src/components/detail/chart/BarChart.vue
b/src/components/detail/chart/BarChart.vue
index 4c0c37e..5af18c5 100644
--- a/src/components/detail/chart/BarChart.vue
+++ b/src/components/detail/chart/BarChart.vue
@@ -13,35 +13,29 @@
import _ from 'lodash';
import config from '../../../config';
+import utils from '../../../utils';
export default {
name: 'bar-chart',
- props: ['breakdown', 'graphModel'],
-
- mounted () {
- this.drawChart();
- },
+ props: ['graphModel', 'data'],
watch: {
- graphModel: {
- handler: function () {
- this.drawChart();
- },
- deep: true
- },
-
- breakdown: function () {
+ data: function () {
this.drawChart();
- }
+ },
},
methods: {
- drawChart () {
- const self = this;
+ // PUBLIC: used by parent components
+ redraw () {
+ this.drawChart();
+ },
- const detail = this.graphModel && this.graphModel.getGraphData();
- if (!detail) return;
+ drawChart () {
+ if (!this.data || !this.data.length) {
+ return;
+ }
const root = d3.select(this.$el),
margin = {top: 6, right: 0, bottom: 20, left: 40},
@@ -55,129 +49,91 @@
'transform', `translate(${margin.left +
padding},${margin.top})`
);
- function resize () {
- const n = root.node();
- let dates = detail.map((d) => new Date(Date.parse(d.month)));
- const datespan = arr.extent(dates);
- const max = _.max(detail.map((r) => {
- if (typeof detail[0].total != 'number') {
- return _.max(_.map(r.total, (breakdownValue, key) => {
- return self.graphModel.getActiveBreakdown()
- .values.find(v => v.key === key).on?
breakdownValue: 0;
+ const n = root.node();
+ const activeDict = this.graphModel.getActiveBreakdownValues();
+ let dates = this.data.map((d) => new Date(Date.parse(d.month)));
+ const datespan = arr.extent(dates);
+
+ const { min, max } = this.graphModel.getMinMax();
+
+ let height = n.offsetHeight - margin.top - margin.bottom - padding;
+ let y = scales.scaleLinear().range([height, 0]);
+ y.domain([min, max]);
+ const yAxis = axes.axisLeft(y).ticks(7)
+
.tickFormat(this.graphModel.formatNumberForMetric.bind(this.graphModel));
+ const yAxisContainer = g.append('g')
+ .call(yAxis)
+ .style('font-size', '13px')
+ .style('font-family', 'Lato, "Open Sans"');
+ const yAxisContainerWidth = yAxisContainer.node().getBBox().width;
+
+ let width = n.offsetWidth - margin.left - margin.right -
yAxisContainerWidth;
+ let xW = scales.scaleBand()
+ .range([0, width])
+ .domain(dates)
+ .paddingOuter(0)
+ .paddingInner(0.1)
+ .align(0);
+
+ svg.attr('width', n.offsetWidth).attr('height', n.offsetHeight);
+
+ let graphElement = g.append('g');
+
+ y.domain([min, max]);
+ graphElement.selectAll('.bar').data(this.data)
+ .enter().selectAll('.minibar').data((d) => {
+ const newData = this.graphModel.activeBreakdown.values
+ .filter(b => b.on)
+ .map((b, i) => ({
+ month: d.month,
+ key: b.name,
+ value: d.total[b.key],
+ color:
config.getColorForBreakdown(this.graphModel.activeBreakdown, b.key,
this.graphModel.config.area),
+ width: xW.bandwidth() /
Object.keys(activeDict).length,
+ index: i
}));
- } else {
- return r.total;
- }
- }));
- const min = Math.min(0, _.min(detail.map((r) => {
- if (typeof detail[0].total != 'number') {
- return _.min(_.map(r.total, (breakdownValue, key) => {
- return self.graphModel.getActiveBreakdown()
- .values.find(v => v.key === key).on?
breakdownValue: 0;
- }));
- } else {
- return r.total;
- }
- })));
- let height = n.offsetHeight - margin.top - margin.bottom -
padding;
- let y = scales.scaleLinear().range([height, 0]);
- y.domain([min, max]);
- const yAxis = axes.axisLeft(y).ticks(7)
-
.tickFormat(self.graphModel.formatNumberForMetric.bind(self.graphModel));
- const yAxisContainer = g.append('g')
- .call(yAxis)
- .style('font-size', '13px')
- .style('font-family', 'Lato, "Open Sans"');
- const yAxisContainerWidth =
yAxisContainer.node().getBBox().width;
- let width = n.offsetWidth - margin.left - margin.right -
yAxisContainerWidth;
- let xW = scales.scaleBand()
- .range([0, width])
- .domain(dates)
- .paddingOuter(0)
- .paddingInner(0.1)
- .align(0);
+ return newData;
+ }).enter().append('rect')
+ .attr('x', (d) => {
+ return xW(d.month) + d.index * d.width;
+ })
+ .attr('y', (d) => {
+ if (d.value >= 0) {
+ return y(d.value);
+ } else {
+ return y(0);
+ }
+ })
+ .attr('width', (d) => d.width)
+ .attr('height', (d) => Math.abs(y(d.value) - y(0)))
+ .attr('fill', (d) => d.color);
- svg.attr('width', n.offsetWidth).attr('height',
n.offsetHeight);
-
- const every = detail.length < 10? 1: 5;
- const period = (dates[1] - dates[0]) < 172800000 ? 'timeDay':
'timeMonth';
- let graphElement = g.append('g');
-
- if (typeof detail[0].total != 'number') {
- y.domain([min, max]);
- graphElement.selectAll('.bar').data(detail)
- .enter().selectAll('.minibar').data(function (d) {
- // this should be passed in
- const breakdown =
self.graphModel.getActiveBreakdown();
- const breakdowns = breakdown.values.filter((x) =>
x.on);
- const newData = breakdowns.map((b, i) => ({
- month: d.month,
- key: b.name,
- value: d.total[b.key],
- color: config.getColorForBreakdown(breakdown,
b.key),
- width: xW.bandwidth() / breakdowns.length,
- index: i
- }));
-
- return newData;
- }).enter().append('rect')
- .attr('x', (d) => {
- return xW(new Date(Date.parse(d.month))) +
d.index * d.width;
- })
- .attr('y', (d) => {
- if (d.value >= 0) {
- return y(d.value);
- } else {
- return y(0);
- }
- })
- .attr('width', (d) => d.width)
- .attr('height', (d) => Math.abs(y(d.value) - y(0)))
- .attr('fill', (d) => d.color);
-
- } else {
- graphElement.selectAll('.bar').data(detail)
- .enter().append('rect')
- .attr('x', (d) => xW(new
Date(Date.parse(d.month))))
- .attr('y', (d) => {
- if (d.total >= 0) {
- return y(d.total);
- } else {
- return y(0);
- }
- })
- .attr('width', xW.bandwidth())
- .attr('height', (d) => Math.abs(y(d.total) - y(0)))
- .attr('fill', (d) =>
self.graphModel.getDarkColor());
- }
- if (min < 0) {
- graphElement.append('line')
- .attr('x1', 0)
- .attr('x2', width)
- .attr('y1', y(0))
- .attr('y2', y(0))
- .style('stroke', 'black')
- .style('stroke-width', 0.5);
- }
- const x = scales.scaleTime()
- .rangeRound([0,
graphElement.node().getBBox().width])
- .domain(datespan);
- const xAxis = axes.axisBottom(x);
- g.append('g').attr('transform', `translate(0,${height})`)
- .call(xAxis)
- .attr('class','x-axis-labels')
- .style('font-size', '13px')
- .style('font-family', 'Lato, "Open Sans"')
- .selectAll("text")
- .style("text-anchor", "end")
- .attr("dx", "-.8em")
- .attr("dy", ".15em")
- .attr("transform", "rotate(-45)");
- svg.attr('width', n.offsetWidth).attr('height',
g.node().getBBox().height + margin.top);
+ if (min < 0) {
+ graphElement.append('line')
+ .attr('x1', 0)
+ .attr('x2', width)
+ .attr('y1', y(0))
+ .attr('y2', y(0))
+ .style('stroke', 'black')
+ .style('stroke-width', 0.5);
}
- resize();
- // TODO: get this to resize cleanly d3.select(window).on('resize',
resize)
+ const x = scales.scaleTime()
+ .rangeRound([0, graphElement.node().getBBox().width])
+ .domain(datespan);
+ const xAxis = axes.axisBottom(x);
+ g.append('g').attr('transform', `translate(0,${height})`)
+ .call(xAxis)
+ .attr('class','x-axis-labels')
+ .style('font-size', '13px')
+ .style('font-family', 'Lato, "Open Sans"')
+ .selectAll("text")
+ .style("text-anchor", "end")
+ .attr("dx", "-.8em")
+ .attr("dy", ".15em")
+ .attr("transform", "rotate(-45)");
+ svg.attr('width', n.offsetWidth).attr('height',
g.node().getBBox().height + margin.top);
+
},
}
}
diff --git a/src/components/detail/chart/LineChart.vue
b/src/components/detail/chart/LineChart.vue
index 346a685..bfca190 100644
--- a/src/components/detail/chart/LineChart.vue
+++ b/src/components/detail/chart/LineChart.vue
@@ -1,16 +1,13 @@
<template>
<div class="graphContainer">
<div v-if="hoveredPoint" class="valuePopup">
- <p ><b>{{formatDate(hoveredPoint.month)}}</b></p>
- <div v-if="!graphModel.getActiveBreakdown()">
- <p>{{selectedValue | thousands}}</p>
- </div>
- <div v-else v-for="b in this.selectedValue">
- <p class="breakdown">
- <b><span v-bind:style="{ color:
getColorForBreakdown(b[0])}">{{graphModel.getActiveBreakdown().values.find(v =>
v.key === b[0]).name + ": "}}</span></b>
- <span>{{b[1] | thousands}}</span>
- </p>
- </div>
+ <b>{{formatDate(hoveredPoint.month)}}</b>
+ <ul v-for="b in this.selectedValue" class="breakdown">
+ <li>
+ <b><span :style="{ color: b.color }">{{b.name}}</span></b>
+ <span>{{b.value | thousands}}</span>
+ </li>
+ </ul>
</div>
<div class="big line chart">
<svg>
@@ -38,35 +35,7 @@
export default {
name: 'line-chart',
- props: ['graphModel'],
-
- mounted () {
- this.drawChart();
- },
-
- computed: {
- selectedValue () {
- if (!this.graphModel.getActiveBreakdown()) {
- return this.hoveredPoint.total;
- } else {
- let l = [];
- _.forEach(this.hoveredPoint.total, (value, key) => {
- if(this.graphModel.getActiveBreakdown().values.find(v =>
v.key === key).on){
- l.push([key, value]);
- }
- });
- return _.sortBy(l, (val) => val[1]).reverse();
- }
- },
- rowData () {
- return this.graphModel.getGraphData().map((row) => {
- return {
- total: row.total,
- month: new Date(row.month)
- };
- });
- }
- },
+ props: ['graphModel', 'data'],
data () {
return {
@@ -74,23 +43,45 @@
};
},
- watch: {
- graphModel: {
- handler: function () {
- this.drawChart();
- },
- deep: true
- },
+ computed: {
+ selectedValue () {
+ let l = [];
+ const activeDict = this.graphModel.getActiveBreakdownValues();
- breakdown: function () {
+ _.forEach(this.hoveredPoint.total, (value, key) => {
+ if(key in activeDict){
+ l.push({
+ key, value,
+ color: this.getColorForBreakdown(key),
+ name: key,
+ });
+ }
+ });
+ return _.sortBy(l, d => d.value).reverse();
+ },
+ },
+
+ mounted () {
+ this.drawChart();
+ },
+
+ watch: {
+ data: function () {
this.drawChart();
}
},
methods: {
+ // PUBLIC: used by parent components
+ redraw () {
+ this.drawChart();
+ },
+
drawChart () {
- const self = this;
+ if (!this.data.length) {
+ return;
+ }
// We make sure that any selected point in a previous chart is
cleared
this.hoveredPoint = null;
@@ -105,9 +96,8 @@
'transform', `translate(${margin.left},${margin.top})`
);
g.selectAll('*').remove();
- const rowData = this.rowData;
- let activeBreakdown = self.graphModel.getActiveBreakdown();
- const max = this.getMaxValue(rowData, activeBreakdown);
+ let activeBreakdown = this.graphModel.activeBreakdown;
+ const { min, max } = this.graphModel.getMinMax();
const n = root.node();
@@ -116,12 +106,12 @@
const width = n.offsetWidth - margin.left - margin.right;
let x = scales.scaleTime().rangeRound([0, width]);
- const dates = rowData.map((d) => d.month);
+ const dates = this.data.map((d) => d.month);
x.domain(arr.extent(dates));
const height = n.offsetHeight - margin.top - margin.bottom -
padding;
let y = scales.scaleLinear().rangeRound([height, 0]);
- y.domain([this.getMinValue(rowData, activeBreakdown), max]);
+ y.domain([min, max]);
// Resize the parent svg element so that it envelops the whole
content
@@ -135,8 +125,8 @@
.x((d) => x(d.month))
.y((d) => y(d.total));
- let breakdownData = [rowData];
- let bColor = this.graphModel.getDarkColor();
+ const activeDict = this.graphModel.getActiveBreakdownValues();
+ let bColor = this.graphModel.darkColor;
/*
Data is flattened if we have breakdowns, so that more than one
line is generated:
@@ -155,11 +145,10 @@
]
*/
- if (activeBreakdown) {
- breakdownData = Object.keys(rowData[0].total).filter(key => {
- return activeBreakdown.values.find(value => value.key ===
key).on;
- }).map((breakdownName) => {
- return rowData.map((row) => {
+ Object.keys(this.data[0].total)
+ .filter(key => key in activeDict)
+ .map((breakdownName) => {
+ return this.data.map((row) => {
return {
month: row.month,
total: row.total[breakdownName],
@@ -167,26 +156,24 @@
};
});
})
- }
- breakdownData.forEach(breakdown => {
+ .forEach(breakdown => {
- // We need to find each breakdown's corresponding colour from
the config
- if (activeBreakdown) {
- bColor = config.getColorForBreakdown(activeBreakdown,
breakdown[0].key);
- }
- g.append('path').datum(breakdown)
- .attr('d', line)
- .style('stroke', '#000')
- .style('stroke-width', '3px')
- .style('fill', 'none');
- g.append('path').datum(breakdown)
- .attr('d', line)
- .attr('class', 'statLine breakdownLine')
- .style('stroke', bColor)
- .style('stroke-width', '2px')
- .style('fill', 'none');
- });
- this.addHoverGuide(g, rowData, x, y);
+ // We need to find each breakdown's corresponding colour
from the config
+ bColor = config.getColorForBreakdown(activeBreakdown,
breakdown[0].key, this.graphModel.config.area);
+ g.append('path').datum(breakdown)
+ .attr('d', line)
+ .style('stroke', '#000')
+ .style('stroke-width', '3px')
+ .style('fill', 'none');
+ g.append('path').datum(breakdown)
+ .attr('d', line)
+ .attr('class', 'statLine breakdownLine')
+ .style('stroke', bColor)
+ .style('stroke-width', '2px')
+ .style('fill', 'none');
+ });
+
+ this.addHoverGuide(g, this.data, x, y);
this.addAxes(x, y, g);
// Final resizing to include the axes
@@ -221,8 +208,7 @@
},
getColorForBreakdown (key) {
- const activeBreakdown = this.graphModel.getActiveBreakdown();
- return config.getColorForBreakdown(activeBreakdown, key);
+ return
config.getColorForBreakdown(this.graphModel.activeBreakdown, key,
this.graphModel.config.area);
},
@@ -240,36 +226,11 @@
this.hoveredPoint = null;
},
- getMaxValue (rowData, activeBreakdown) {
- if (activeBreakdown) {
- return _.max(rowData.map((r) => {
- return _.max(_.map(r.total, (breakdownValue, key) => {
- return activeBreakdown.values
- .find(v => v.key === key).on?
breakdownValue: 0;
- }));
- }));
- } else {
- return _.max(rowData.map((d) => d.total));
- }
- },
-
- getMinValue (rowData, activeBreakdown) {
- if (activeBreakdown) {
- return Math.min(0,_.min(rowData.map((r) => {
- return _.min(_.map(r.total, (breakdownValue, key) => {
- return activeBreakdown.values
- .find(v => v.key === key).on?
breakdownValue: 0;
- }));
- })));
- } else {
- return Math.min(0, _.min(rowData.map((d) => d.total)));
- }
- },
-
// The hover guide is a UI element that shows the value of each point
in the
// line when hovering. It adds a vertical line for better feedback.
addHoverGuide (g, rowData, x, y) {
- let self = this;
+ const self = this;
+
const width = x.range()[1];
const height = y.range()[0];
this.addGuideLine(g, height);
@@ -298,26 +259,19 @@
// For better clarity on which point is being selected, we add
circles that
// indicate the exact one.
+ const activeDict = this.graphModel.getActiveBreakdownValues();
hoverGs.each(function (d) {
let sel = d3.select(this);
- const activeBreakdown = self.graphModel.getActiveBreakdown();
- if (activeBreakdown) {
- _.forEach(d.total, (value, key) => {
- if (activeBreakdown.values.find(v => v.key ===
key).on) {
- sel.append('circle')
- .attr('cx', d => x(d.month))
- .attr('cy', d => y(value))
- .attr('r', 5)
- .style('display', 'none');
- }
- });
- } else {
- sel.append('circle')
- .attr('cx', d => x(d.month))
- .attr('cy', d => y(d.total))
- .attr('r', 5)
- .style('display', 'none');
- }
+ const activeBreakdown = self.graphModel.activeBreakdown;
+ _.forEach(d.total, (value, key) => {
+ if (key in activeDict) {
+ sel.append('circle')
+ .attr('cx', d => x(d.month))
+ .attr('cy', d => y(value))
+ .attr('r', 5)
+ .style('display', 'none');
+ }
+ });
});
},
diff --git a/src/components/detail/chart/TableChart.vue
b/src/components/detail/chart/TableChart.vue
index 908e954..bd6856c 100644
--- a/src/components/detail/chart/TableChart.vue
+++ b/src/components/detail/chart/TableChart.vue
@@ -1,38 +1,24 @@
<template>
<div>
- <table :class="metricData.area" class="ui table" v-if="!breakdown">
+ <table :class="graphModel.config.area" class="ui table">
<thead>
- <tr v-if="['bars', 'lines'].includes(metricData.type)">
+ <tr v-if="['bars', 'lines'].includes(graphModel.config.type)">
<th>Date</th>
- <th>Total</th>
+ <th class="right aligned" v-for="v in
graphModel.activeBreakdown.values" v-if="v.on">{{v.name}}</th>
</tr>
- <tr v-if="metricData.type === 'list'">
- <th>{{metricData.value}}</th>
+ <tr v-if="graphModel.config.type === 'list'">
+ <th class="right aligned">{{graphModel.config.valueName}}</th>
<th>Name</th>
</tr>
</thead>
<tbody>
- <tr v-if="['bars', 'lines'].includes(metricData.type)" v-for="m in
listData">
+ <tr v-if="['bars', 'lines'].includes(graphModel.config.type)"
v-for="m in data">
<td>{{m.month|date}}</td>
- <td>{{m.total|thousands}}</td>
+ <td class="right aligned" v-for="v in
graphModel.activeBreakdown.values" v-if="v.on">{{m.total[v.key]|thousands}}</td>
</tr>
- <tr v-if="metricData.type === 'list'" v-for="m in listData">
- <td class="right
aligned">{{m[metricData.value]|thousands}}</td>
- <td><a target="_blank" :href="'\/\/' + $store.state.project +
'/wiki/' + m[metricData.key]">{{m[metricData.key].replace(/_/g, ' ')}}</a></td>
- </tr>
- </tbody>
- </table>
- <table :class="metricData.area" class="ui table" v-if="breakdown">
- <thead>
- <tr>
- <th>Date</th>
- <th v-for="v in breakdown.values" v-if="v.on">{{v.name}}</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="m in listData">
- <td>{{m.month|date}}</td>
- <td v-for="v in breakdown.values"
v-if="v.on">{{m.total[v.key]|thousands}}</td>
+ <tr v-if="graphModel.config.type === 'list'" v-for="m in data">
+ <td class="right
aligned">{{m[graphModel.config.value].total|thousands}}</td>
+ <td><a target="_blank" :href="'\/\/' + $store.state.project +
'/wiki/' + m[graphModel.config.key]">{{m[graphModel.config.key].replace(/_/g, '
')}}</a></td>
</tr>
</tbody>
</table>
@@ -42,7 +28,7 @@
<script>
export default {
name: 'table-chart',
- props: ['metricData', 'graphModel'],
+ props: ['data', 'graphModel'],
mounted () {
this.setColors();
@@ -52,26 +38,13 @@
this.setColors();
},
- computed: {
- listData () {
- if (this.metricData.type === 'list') {
- return this.graphModel.topXByY(this.metricData.key,
this.metricData.value).slice(0, 100);
- } else {
- return this.graphModel.getGraphData();
- }
- },
- breakdown () {
- return this.graphModel.getActiveBreakdown();
- }
- },
-
methods: {
setColors () {
const headerCells = this.$el.querySelectorAll('th');
let i = null;
for (let i = 0; i < headerCells.length; i++) {
- headerCells[i].style = `background-color:
${this.metricData.darkColor};`;
+ headerCells[i].style = `background-color:
${this.graphModel.config.darkColor};`;
}
}
}
diff --git a/src/config/index.js b/src/config/index.js
index a14f746..df55ea3 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -17,9 +17,9 @@
];
const colors = {
- contributing: ['#c4cddf', '#99afd9', '#6582ba', '#2a4b8d'],
- reading: ['#c8f0e7', '#77d8c2', '#00af89', '#03745c'],
- content: ['#fff1c6', '#f9df90', '#ffcc33', '#ddad1c']
+ contributing: ['#C4CDDF', '#99AFD9', '#6582BA', '#2A4B8D'],
+ reading: ['#C8F0E7', '#77D8C2', '#00AF89', '#03745C'],
+ content: ['#FFF1C6', '#F9DF90', '#FFCC33', '#DDAD1C']
};
const qualitativeScale = {
@@ -130,6 +130,8 @@
export default {
+ ALL_PROJECTS: 'all-projects',
+
sitematrix: {
endpoint:
'https://meta.wikimedia.org/w/api.php?action=sitematrix&formatversion=2&format=json&maxage=3600&smaxage=3600'
},
@@ -187,8 +189,10 @@
);
},
- getColorForBreakdown (breakdown, key) {
- return
qualitativeScale[breakdown.values.length][breakdown.values.indexOf(breakdown.values.find(value
=> value.key === key))]
+ getColorForBreakdown (breakdown, key, area) {
+ return key === 'total' ?
+ colors[area][colors[area].length - 1] :
+
qualitativeScale[breakdown.values.length][breakdown.values.indexOf(breakdown.values.find(value
=> value.key === key))];
},
areas () {
@@ -218,5 +222,5 @@
stableColorIndexes,
questions,
areasWithMetrics,
- months
+ months,
};
diff --git a/src/config/metrics/content.js b/src/config/metrics/content.js
index 2943fae..686a187 100644
--- a/src/config/metrics/content.js
+++ b/src/config/metrics/content.js
@@ -20,7 +20,6 @@
unit: 'bytes',
global: true,
breakdowns: [{
- on: false,
name: 'User type',
breakdownName: 'editor_type',
values: [
@@ -30,7 +29,6 @@
{ name: 'User', on: true, key: 'user' },
]
},{
- on: false,
name: 'Page type',
breakdownName: 'page_type',
values: [
@@ -61,7 +59,6 @@
value: 'edited_pages',
global: false,
breakdowns: [{
- on: false,
name: 'Editor type',
breakdownName: 'editor_type',
values: [
@@ -71,7 +68,6 @@
{ name: 'User', on: true, key: 'user' },
]
},{
- on: false,
name: 'Page type',
breakdownName: 'page_type',
values: [
@@ -79,7 +75,6 @@
{ name: 'Non content', on: true, key: 'non-content' }
]
},{
- on: false,
name: 'Activity level',
breakdownName: 'activity_level',
values: [
@@ -112,7 +107,6 @@
value: 'net_bytes_diff',
global: true,
breakdowns: [{
- on: false,
name: 'User type',
breakdownName: 'editor_type',
values: [
@@ -122,7 +116,6 @@
{ name: 'User', on: true, key: 'user' },
]
},{
- on: false,
name: 'Page type',
breakdownName: 'page_type',
values: [
diff --git a/src/config/metrics/contributing.js
b/src/config/metrics/contributing.js
index 696daea..8476969 100644
--- a/src/config/metrics/contributing.js
+++ b/src/config/metrics/contributing.js
@@ -21,7 +21,6 @@
value: 'editors',
global: false,
breakdowns: [{
- on: false,
name: 'Editor type',
breakdownName: 'editor_type',
values: [
@@ -31,7 +30,6 @@
{ name: 'User', on: true, key: 'user' },
]
},{
- on: false,
name: 'Page type',
breakdownName: 'page_type',
values: [
@@ -39,7 +37,6 @@
{ name: 'Non content', on: true, key: 'non-content' }
]
},{
- on: false,
name: 'Activity level',
breakdownName: 'activity_level',
values: [
@@ -71,7 +68,6 @@
value: 'edits',
global: true,
breakdowns: [{
- on: false,
name: 'Editor type',
breakdownName: 'editor_type',
values: [
@@ -81,7 +77,6 @@
{ name: 'User', on: true, key: 'user' },
]
},{
- on: false,
name: 'Page type',
breakdownName: 'page_type',
values: [
@@ -112,7 +107,6 @@
value: 'new_pages',
global: true,
breakdowns: [{
- on: false,
name: 'Editor type',
breakdownName: 'editor_type',
values: [
@@ -122,7 +116,6 @@
{ name: 'User', on: true, key: 'user' },
]
},{
- on: false,
name: 'Page type',
breakdownName: 'page_type',
values: [
diff --git a/src/config/metrics/metricSchema.js
b/src/config/metrics/metricSchema.js
index 4f19368..d849fc4 100644
--- a/src/config/metrics/metricSchema.js
+++ b/src/config/metrics/metricSchema.js
@@ -53,10 +53,6 @@
type: 'array',
required: false,
schema: {
- on: {
- type: 'boolean',
- required: true
- },
name: {
type: 'string',
required: true
@@ -81,4 +77,4 @@
}
};
-export default metricSchema;
\ No newline at end of file
+export default metricSchema;
diff --git a/src/config/metrics/reading.js b/src/config/metrics/reading.js
index d2691f1..48de7f0 100644
--- a/src/config/metrics/reading.js
+++ b/src/config/metrics/reading.js
@@ -43,7 +43,6 @@
value: 'views',
global: true,
breakdowns: [{
- on: false,
name: 'Access method',
breakdownName: 'access',
values: [
@@ -74,7 +73,6 @@
area: 'reading',
global: false,
breakdowns: [{
- on: false,
name: 'Access site',
breakdownName: 'access-site',
values: [
diff --git a/src/lodash-custom-bundle.js b/src/lodash-custom-bundle.js
index b790524..934d83d 100644
--- a/src/lodash-custom-bundle.js
+++ b/src/lodash-custom-bundle.js
@@ -11,6 +11,8 @@
import kebabCase from 'lodash/kebabCase';
import last from 'lodash/last';
import map from 'lodash/map';
+import max from 'lodash/max';
+import min from 'lodash/min';
import pickBy from 'lodash/pickBy';
import replace from 'lodash/replace';
import round from 'lodash/round';
@@ -18,6 +20,7 @@
import startsWith from 'lodash/startsWith';
import sum from 'lodash/sum';
import take from 'lodash/take';
+import toPairs from 'lodash/toPairs';
import transform from 'lodash/transform';
import trim from 'lodash/trim';
import zip from 'lodash/zip';
@@ -37,6 +40,8 @@
kebabCase,
last,
map,
+ max,
+ min,
pickBy,
replace,
round,
@@ -44,6 +49,7 @@
startsWith,
sum,
take,
+ toPairs,
transform,
trim,
zip,
diff --git a/src/models/DimensionalData.js b/src/models/DimensionalData.js
index b1ba2b1..15026d4 100644
--- a/src/models/DimensionalData.js
+++ b/src/models/DimensionalData.js
@@ -85,7 +85,8 @@
return Object.keys(breakDownMap).map((key) => {
let row = {}
row[measure] = key;
- row[column] = breakDownMap[key];
+ // subtle format normalization so that all breakdowns look the
same
+ row[column] = { total: breakDownMap[key] };
return row;
});
} else {
diff --git a/src/models/GraphModel.js b/src/models/GraphModel.js
index 6910916..5d9fc70 100644
--- a/src/models/GraphModel.js
+++ b/src/models/GraphModel.js
@@ -1,69 +1,73 @@
import _ from '../lodash-custom-bundle';
import numeral from 'numeral';
+import utils from '../utils';
class GraphModel {
- constructor (metricData, dimensionalData, prevBreakdowns) {
- this.metricData = metricData;
- this.dimensionalData = dimensionalData;
- if (prevBreakdowns) {
- this.breakdowns = prevBreakdowns;
- } else {
- this.breakdowns = this.metricData.breakdowns &&
JSON.parse(JSON.stringify(this.metricData.breakdowns));
+ constructor (configuration) {
+ this.config = configuration;
+
+ if (this.config.type === 'list') {
+ return;
}
+
+ this.breakdowns = utils.cloneDeep(this.config.breakdowns || []);
+ // insert a "total" breakdown as a default breakdown
+ this.breakdowns.splice(0, 0, {
+ total: true,
+ name: 'Total',
+ // this undefined is meaningful as a second parameter to
DimensionalData.breakdown
+ breakdownName: null,
+ values: [
+ { name: 'total', on: true, key: 'total' },
+ ],
+ })
+ this.activeBreakdown = this.breakdowns[0];
+
+ // TODO: maybe make this dynamic when the breakdown is activated?
// Remove dimension values that have no data.
- const breakdown = this.getActiveBreakdown();
- if (breakdown) {
- let dimensionValues =
this.dimensionalData.getDimensionValues(breakdown.breakdownName);
+ /*
+ this.breakdowns.forEach(breakdown => {
+ const dimensionValues =
this.data.getDimensionValues(breakdown.breakdownName);
breakdown.values = _.filter(breakdown.values, item =>
dimensionValues.includes(item.key));
- }
+ });
+ */
+
+ this.graphData = [];
}
- getGraphData () {
- if (this.metricData.type === 'list') {
- return this.topXByY(this.metricData.key, this.metricData.value);
+
+ setData (data) {
+ this.data = data;
+
+ if (this.config.type === 'list') {
+ this.graphData = this.topXByY();
+ return;
}
const xAxisValue = 'timestamp';
- const yAxisValue = this.metricData.value;
- this.dimensionalData.measure('timestamp');
- const activeBreakdown = this.getActiveBreakdown();
- if (activeBreakdown) {
- // TODO: individual breakdown values should be filtered with
DimensionalData
- let brokenDownValues = [];
- const rawValues = this.dimensionalData.breakdown(yAxisValue,
activeBreakdown.breakdownName);
- return rawValues.map((row) => {
- var ts = row.timestamp;
- const month = createDate(ts);
- return {month: month, total: row[yAxisValue]};
- });
- } else {
- const rawValues = this.dimensionalData.breakdown(yAxisValue);
- return rawValues.map((row) => {
- var ts = row.timestamp;
- const month = createDate(ts);
- return {month: month, total: row[yAxisValue]}
- });
- }
+ const yAxisValue = this.config.value;
+
+ this.data.measure(xAxisValue);
+ const rawValues = this.data.breakdown(yAxisValue,
this.activeBreakdown.breakdownName);
+
+ this.graphData = rawValues.map((row) => {
+ var ts = row.timestamp;
+ const month = createDate(ts);
+ return {month: month, total: row[yAxisValue]};
+ });
}
- getMetricBreakdowns () {
- return this.metricData.breakdowns;
+
+ refreshData () {
+ this.setData(this.data);
}
- getBreakdowns () {
- return this.breakdowns;
+
+ get area () {
+ return this.config.area;
}
- getArea () {
- return this.metricData.area;
- }
- getDarkColor () {
- return this.metricData.darkColor;
- }
- getActiveBreakdown () {
- if (!this.breakdowns) return null;
- return this.breakdowns.filter((breakdown) => {
- return breakdown.on;
- })[0];
+ get darkColor () {
+ return this.config.darkColor;
}
getAggregateLabel () {
- return this.metricData.additive ? 'Total' : 'Average';
+ return this.config.additive ? 'Total' : 'Average';
}
getAggregate () {
@@ -75,33 +79,50 @@
const total = _.sum(values);
const average = _.round(total / values.length, 1);
- return this.metricData.additive ? total : average;
+ return this.config.additive ? total : average;
}
getAggregatedValues (limitToLastN) {
- const data = this.getGraphData();
- let values;
-
- if (typeof data[0].total === 'number') {
- values = data.map((c) => {
- return c.total;
- });
- } else {
- values = data.map((r) => {
- return _.sum(_.map(r.total, (breakdownValue, key) => {
- return this.getActiveBreakdown().values.find(v => v.key
=== key).on? breakdownValue: 0;
- }));
- });
- }
+ const activeDict = this.getActiveBreakdownValues();
+ const values = this.graphData.map((d) => {
+ return _.sum(_.map(d.total, (breakdownValue, key) => {
+ return key in activeDict ? breakdownValue : 0;
+ }));
+ });
const limit = Math.min(limitToLastN || values.length, values.length);
return _.take(values, limit);
}
- topXByY (x, y) {
- this.dimensionalData.measure(x);
- return _.sortBy(this.dimensionalData.breakdown(y), y).reverse();
+
+ getActiveBreakdownValues () {
+ const actives = this.activeBreakdown.values.filter(bv => bv.on).map(bv
=> bv.key);
+ return actives.reduce((r, a) => { r[a] = true; return r; }, {});
}
+
+ getMinMax () {
+ const activeDict = this.getActiveBreakdownValues();
+ let min = 0;
+ let max = 0;
+
+ _.forEach(this.graphData, d => {
+ const active = _.toPairs(d.total).filter(r => r[0] in
activeDict).map(r => r[1]);
+ min = Math.min(min, _.min(active));
+ max = Math.max(max, _.max(active));
+ });
+
+ return { min, max };
+ }
+
+ topXByY (limit) {
+ const x = this.config.key;
+ const y = this.config.value;
+
+ this.data.measure(x);
+ const results = this.data.breakdown(y);
+ return _.take(_.sortBy(results, y).reverse(), limit || results.length);
+ }
+
formatNumberForMetric (number) {
- if (this.metricData.unit === 'bytes') {
+ if (this.config.unit === 'bytes') {
return numeral(number).format('0,0b');
} else {
return numeral(number).format('0,0a');
diff --git a/src/router/index.js b/src/router/index.js
index f6ab4fe..0e0f046 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -144,7 +144,7 @@
let state = getStateFromPath(path, routes);
windowObject.history.replaceState(state, '', getPathFromState(root,
state, routes));
let mainComponent = getMainComponentFromState(state, routes);
- store.commit('resetState', Object.assign({mainComponent}, state));
+ store.commit('resetNavigationState', Object.assign({mainComponent},
state));
// Subscribe to changes on the application state.
store.watch(
@@ -158,7 +158,7 @@
// Update the application state with the redirect
// without propagating the change to browser history
// or updating the main component.
- store.commit('resetState', redirectedState);
+ store.commit('resetNavigationState', redirectedState);
} else {
// Update only the main component and the browser's
location.
store.commit('setState', {
@@ -175,7 +175,7 @@
// Subscribe to changes on the browser's location.
windowObject.onpopstate = function (event) {
if (event.state) {
- store.commit('resetState', event.state);
+ store.commit('resetNavigationState', event.state);
}
};
}
diff --git a/src/router/routes.js b/src/router/routes.js
index 8d0528c..b5203cf 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -20,7 +20,7 @@
* be 2 routes with the same set of wildcards.
*/
const routes = [
- ['/', { redirect: '/all-projects' }],
+ ['/', { redirect: '/' + config.ALL_PROJECTS }],
['/:project', { mainComponent: 'dashboard' }],
['/:project/:area', { redirect: getDefaultMetricPath }],
['/:project/:area/:metric', { mainComponent: 'detail' }],
diff --git a/src/store/index.js b/src/store/index.js
index 038581d..298f9dc 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -10,7 +10,6 @@
area: '',
metric: '',
mainComponent: '',
- breakdowns: null,
topicsMinimized: true,
centralNotice: null,
},
@@ -31,8 +30,9 @@
Object.keys(arg).forEach(k => state[k] = arg[k]);
},
// Sets all poperties passed, and sets any remaining navigation
properties to empty string.
- resetState (state, arg) {
+ resetNavigationState (state, arg) {
navigationStateKeys.forEach(k => state[k] = '');
+ state.activeBreakdown = null;
Object.keys(arg).forEach(k => state[k] = arg[k]);
},
},
diff --git a/src/utils.js b/src/utils.js
index dd04852..7a54cdc 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -16,6 +16,11 @@
const formatSi = format.format(".2s");
+function cloneDeep (c) {
+ return JSON.parse(JSON.stringify(c));
+}
+
export default {
- labeledCrossProduct
+ labeledCrossProduct,
+ cloneDeep,
};
diff --git a/test/DimensionalData.spec.js b/test/DimensionalData.spec.js
index 656bb70..582f669 100644
--- a/test/DimensionalData.spec.js
+++ b/test/DimensionalData.spec.js
@@ -46,10 +46,10 @@
let dim = new DimensionalData(pageviews1)
dim.measure('date')
let break1 = dim.breakdown('views')
- expect(break1.find((x) => x.date === '2017-01').views).toEqual(23)
+ expect(break1.find((x) => x.date ===
'2017-01').views.total).toEqual(23)
dim.merge(pageviews2)
let break2 = dim.breakdown('views')
- expect(break2.find((x) => x.date === '2017-01').views).toEqual(24)
+ expect(break2.find((x) => x.date ===
'2017-01').views.total).toEqual(24)
});
it('should break down by two columns', function () {
diff --git a/test/GraphModel.spec.js b/test/GraphModel.spec.js
index da17e10..38d62c2 100644
--- a/test/GraphModel.spec.js
+++ b/test/GraphModel.spec.js
@@ -24,16 +24,18 @@
describe('GraphModel', function () {
it('should reflect basic properties', function () {
- let graphModel = new GraphModel(metric, dimensionalData);
+ let graphModel = new GraphModel(metric);
+ graphModel.setData(dimensionalData);
- expect(graphModel.getArea()).toEqual(metric.area);
-
expect(graphModel.getBreakdowns()[0].name).toEqual(metric.breakdowns[0].name);
+ expect(graphModel.area).toEqual(metric.area);
+
expect(graphModel.breakdowns[1].name).toEqual(metric.breakdowns[0].name);
});
it('should aggregate total when metric is additive', function () {
metric.additive = true;
- let graphModel = new GraphModel(metric, dimensionalData);
+ let graphModel = new GraphModel(metric);
+ graphModel.setData(dimensionalData);
expect(graphModel.getAggregateLabel()).toEqual('Total');
expect(graphModel.getAggregate()).toEqual(1449174299);
@@ -44,7 +46,8 @@
it('should aggregate average when metric is not additive', function () {
metric.additive = false;
- let graphModel = new GraphModel(metric, dimensionalData);
+ let graphModel = new GraphModel(metric);
+ graphModel.setData(dimensionalData);
expect(graphModel.getAggregateLabel()).toEqual('Average');
expect(graphModel.getAggregate()).toEqual(120764524.9);
});
@@ -52,34 +55,32 @@
it('should total properly when breaking down', function () {
metric.additive = true;
- metric.breakdowns[0].on = true;
- metric.breakdowns[0].values[0].on = true;
- metric.breakdowns[0].values[1].on = false;
+ let graphModel = new GraphModel(metric);
+ graphModel.activeBreakdown = graphModel.breakdowns[1];
+ graphModel.setData(dimensionalData);
- let graphModel = new GraphModel(metric, dimensionalData);
+ graphModel.activeBreakdown.values[0].on = true;
+ graphModel.activeBreakdown.values[1].on = false;
expect(graphModel.getAggregate()).toEqual(882978744);
- metric.breakdowns[0].values[0].on = false;
- metric.breakdowns[0].values[1].on = true;
-
- graphModel = new GraphModel(metric, dimensionalData);
+ graphModel.activeBreakdown.values[0].on = false;
+ graphModel.activeBreakdown.values[1].on = true;
expect(graphModel.getAggregate()).toEqual(566195555);
});
it('should average properly when breaking down', function () {
metric.additive = false;
- metric.breakdowns[0].on = true;
- metric.breakdowns[0].values[0].on = true;
- metric.breakdowns[0].values[1].on = false;
+ let graphModel = new GraphModel(metric);
+ graphModel.activeBreakdown = graphModel.breakdowns[1];
+ graphModel.setData(dimensionalData);
- let graphModel = new GraphModel(metric, dimensionalData);
+ graphModel.activeBreakdown.values[0].on = true;
+ graphModel.activeBreakdown.values[1].on = false;
expect(graphModel.getAggregate()).toEqual(73581562);
- metric.breakdowns[0].values[0].on = false;
- metric.breakdowns[0].values[1].on = true;
-
- graphModel = new GraphModel(metric, dimensionalData);
+ graphModel.activeBreakdown.values[0].on = false;
+ graphModel.activeBreakdown.values[1].on = true;
expect(graphModel.getAggregate()).toEqual(47182962.9);
});
});
diff --git a/test/Router.spec.js b/test/Router.spec.js
index ea83044..d067e43 100644
--- a/test/Router.spec.js
+++ b/test/Router.spec.js
@@ -164,7 +164,7 @@
'/#/foo/bar',
);
expect(storeMock.commit).toHaveBeenCalledWith(
- 'resetState',
+ 'resetNavigationState',
{foo: 'foo', bar: 'bar', mainComponent: 'foo-bar'},
);
});
@@ -210,7 +210,7 @@
watchCallback(newState);
expect(storeMock.commit).toHaveBeenCalledWith(
- 'resetState',
+ 'resetNavigationState',
{foo: 'foo', bar: 'bar', qux: 'qux'},
);
});
@@ -227,7 +227,7 @@
windowMock.onpopstate({state: newState});
expect(storeMock.commit).toHaveBeenCalledWith(
- 'resetState',
+ 'resetNavigationState',
{foo: 'foo', bar: 'bar'},
);
});
diff --git a/test/components/LineChart.spec.js
b/test/components/LineChart.spec.js
index 538bc9e..d7ab31b 100644
--- a/test/components/LineChart.spec.js
+++ b/test/components/LineChart.spec.js
@@ -10,7 +10,6 @@
value: 'devices',
area: 'reading',
breakdowns: [{
- on: false,
name: 'Access site',
breakdownName: 'access-site',
values: [
@@ -22,57 +21,48 @@
let dimensionalData = new DimensionalData(uniques.desktop.items);
dimensionalData.merge(uniques.mobile.items);
-const graphModel = new GraphModel(metric, dimensionalData);
+const graphModel = new GraphModel(metric);
+graphModel.setData(dimensionalData);
let vm;
describe('The line chart', () => {
beforeEach(() => {
vm = new Vue({
- template: '<div><test :graphModel="graphModel"></test></div>',
+ template: '<div><test :graphModel="graphModel"
:data="graphModel.graphData"></test></div>',
components: {
'test': LineChart
},
data () {
return {
- graphModel: graphModel
+ graphModel
}
}
}).$mount();
})
it('should generate one line when there are no breakdowns selected', () =>
{
- const lineClassName = '.statLine';
- expect($(lineClassName, vm.$el).length).toEqual(1);
+ expect($('.breakdownLine', vm.$el).length).toEqual(1);
});
it('should generate as many lines as breakdowns selected', () => {
- const breakdown = {
- on: true,
- name: 'Access site',
- breakdownName: 'access-site',
- values: [
- { name: 'Mobile Site', on: true, key: 'mobile-site' },
- { name: 'Desktop Site', on: true, key: 'desktop-site' }
- ]
- }
- graphModel.breakdowns[0] = breakdown;
+ graphModel.activeBreakdown = graphModel.breakdowns[1];
+ // refresh data (not ideal)
+ graphModel.refreshData();
const vm = new Vue({
- template: '<div><test :graphModel="graphModel"></test></div>',
+ template: '<div><test :graphModel="graphModel"
:data="graphModel.graphData"></test></div>',
components: {
'test': LineChart
},
data () {
return {
- graphModel: graphModel
+ graphModel
}
}
}).$mount();
- const breakdownLineClassName = '.breakdownLine';
- expect($(breakdownLineClassName, vm.$el).length).toEqual(2);
+ expect($('.breakdownLine', vm.$el).length).toEqual(2);
});
it('should generate an x axis', function () {
- const xAxisClassName = '.xAxisLabel';
- expect($(xAxisClassName, vm.$el).length).toBeGreaterThan(0);
+ expect($('.xAxisLabel', vm.$el).length).toBeGreaterThan(0);
});
});
--
To view, visit https://gerrit.wikimedia.org/r/391490
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I034b70b3d5a0034aa87013650884691c1c94e9f9
Gerrit-PatchSet: 11
Gerrit-Project: analytics/wikistats2
Gerrit-Branch: master
Gerrit-Owner: Milimetric <[email protected]>
Gerrit-Reviewer: Fdans <[email protected]>
Gerrit-Reviewer: Mforns <[email protected]>
Gerrit-Reviewer: Milimetric <[email protected]>
Gerrit-Reviewer: Nuria <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits