Skip to content

Commit

Permalink
Add links on outings stats page
Browse files Browse the repository at this point in the history
  • Loading branch information
cbeauchesne committed Apr 21, 2021
1 parent 8f460f6 commit 9f526f7
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 16 deletions.
120 changes: 119 additions & 1 deletion src/views/portals/outings-stats/OutingsStatsPart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@
<script>
import { Histogram, StackedHistogram } from './outings-stats';
import constants from '@/js/constants';
import common from '@/js/constants/common.json';
const NOT_NULL_VALUES = '__NOT_NULL_VALUES__';
const getOutingYear = function (outing) {
return parseInt(outing.date_start.substring(0, 4), 10);
};
Expand Down Expand Up @@ -151,7 +154,10 @@ export default {
outings: {
type: Array,
required: true,
tye: Array,
},
activity: {
type: String,
default: null,
},
},
Expand All @@ -164,55 +170,167 @@ export default {
},
methods: {
urlBuilder(fields) {
/*
* ## The "magic" function that build URLs.
*
* it hides all the field query complexity, and expose a simple prototype
* It also handles overwriting the actuel $route possible filter
*
* ### Basic behavior
*
* Take an object with field ids (like `ski_rating`) and values, it will returns and URL:
*
* { activities: "skitouring" } => /outings?act=skitouring
*
* ### `year` id
*
* Even if `year` is not a field id, it's accepted, and the value is transformed. Example:
*
* { year: 2013} => /outings?date=2013-01-01,2013-12-31
*
* ### `null` value
*
* If the value is NOT_NULL_VALUES, then the lower and upper bound of possible values is used. It's usefull to
* filter on outings where the field is defined, whetever the value (e.g. NOT NULL ). Example:
*
* { ski_rating: NOT_NULL_VALUES } => /outings?trat=1.1,5.6
* { height_diff_up: NOT_NULL_VALUES } => /outings?odif=0,9000
*
* ### When field is requested threw a range
*
* It seems that the API is not able to filter on a single value on some field. When the query mode of the field
* is a slided (meaning setting an lower and an upper bound, then we use a range). Example :
*
* { rock_free_rating: "5a" } => /outings?frat=5a,5a
*
*/
const queryArgs = {};
const outingFields = constants.objectDefinitions.outing.fields;
// specific treatment for year
if (fields.year) {
queryArgs.date = `${fields.year}-01-01,${fields.year}-12-31`;
delete fields.year;
}
for (const fieldId in fields) {
const field = outingFields[fieldId];
const value = fields[fieldId];
if (value === NOT_NULL_VALUES) {
// when value is null, get bounds of possible values
if (field.values) {
queryArgs[field.url] = `${field.values[0]},${field.values[field.values.length - 1]}`;
} else {
queryArgs[field.url] = `${field.min},${field.max}`;
}
} else if (field.queryMode === 'valuesRangeSlider') {
// is field's query mode is a range slider (means, we query it threw a range) tthen use a range
queryArgs[field.url] = `${fields[fieldId]},${fields[fieldId]}`;
} else {
// otherwise, simply take the value
queryArgs[field.url] = fields[fieldId];
}
}
// get current route query, and overwrite it with filters in arguments
const query = { ...this.$route.query, ...queryArgs };
// if the current tab is filterd with activities
if (this.activity) {
query.act = this.activity;
}
// build target route object, and return the full path
const route = this.$router.resolve({ name: 'outings', query }).route;
return route.fullPath;
},
createGraphs() {
// we can't filter outings containing two activities
// so we display a filter on legend only for the first part (all activities)
const categoryUrlGetter = this.activity ? null : (category) => this.urlBuilder({ activities: category });
new StackedHistogram(this.outings, this.$refs.year_repartition, getOutingYear, (d) => d.activities)
.color(getActivityColor)
.categoryUrl(categoryUrlGetter)
.categoryLabel((activity) => this.$gettext(activity, 'activities'))
.xUrl((year) => this.urlBuilder({ year }))
.dataUrl(this.activity ? null : (year, category) => this.urlBuilder({ year, activities: category }))
.draw();
new StackedHistogram(this.outings, this.$refs.month_repartition, getOutingMonth, (d) => d.activities)
.color(getActivityColor)
.xTickLabel(this.$dateUtils.month)
.categoryUrl(categoryUrlGetter)
.categoryLabel((activity) => this.$gettext(activity, 'activities'))
.xDomain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) // always display all months
.draw();
new Histogram(this.outings, this.$refs.height_diff_up, getOutingYear)
.y((outing) => outing.height_diff_up)
.yTickLabel(formatLengthInMeter)
.xUrl((year) => this.urlBuilder({ year, height_diff_up: NOT_NULL_VALUES }))
.draw();
new Histogram(this.outings, this.$refs.height_diff_difficulties, getOutingYear)
.y((outing) => outing.height_diff_difficulties)
.yTickLabel(formatLengthInMeter)
.xUrl((year) =>
this.urlBuilder({
year,
height_diff_difficulties: NOT_NULL_VALUES,
activities: 'mountain_climbing,snow_ice_mixed',
})
)
.draw();
new StackedHistogram(this.outings, this.$refs.rock_free_rating, getOutingYear, (d) => [d.rock_free_rating])
.color(getRockRatingColor)
.categoryUrl((rock_free_rating) => this.urlBuilder({ rock_free_rating }))
.xUrl((year) => this.urlBuilder({ year, rock_free_rating: NOT_NULL_VALUES }))
.dataUrl((year, rock_free_rating) => this.urlBuilder({ year, rock_free_rating }))
.draw();
new StackedHistogram(this.outings, this.$refs.global_rating, getOutingYear, (d) => [d.global_rating])
.color(getGlobalRatingColor)
.categoryUrl((global_rating) => this.urlBuilder({ global_rating }))
.categoryComparator(compareGlobalRatings)
.xUrl((year) => this.urlBuilder({ year, global_rating: NOT_NULL_VALUES }))
.dataUrl((year, global_rating) => this.urlBuilder({ year, global_rating }))
.draw();
new StackedHistogram(this.outings, this.$refs.labande_global_rating, getOutingYear, (d) => [
d.labande_global_rating,
])
.color(getGlobalRatingColor)
.categoryUrl((labande_global_rating) => this.urlBuilder({ labande_global_rating }))
.categoryComparator(compareGlobalRatings)
.xUrl((year) => this.urlBuilder({ year, labande_global_rating: NOT_NULL_VALUES }))
.dataUrl((year, labande_global_rating) => this.urlBuilder({ year, labande_global_rating }))
.draw();
new StackedHistogram(this.outings, this.$refs.ski_rating, getOutingYear, (d) => [d.ski_rating])
.color(getSkiRatingColor)
.categoryUrl((ski_rating) => this.urlBuilder({ ski_rating }))
.xUrl((year) => this.urlBuilder({ year, ski_rating: NOT_NULL_VALUES }))
.dataUrl((year, ski_rating) => this.urlBuilder({ year, ski_rating }))
.draw();
new StackedHistogram(this.outings, this.$refs.ice_rating, getOutingYear, (d) => [d.ice_rating])
.color(getIceRatingColor)
.categoryUrl((ice_rating) => this.urlBuilder({ ice_rating }))
.xUrl((year) => this.urlBuilder({ year, ice_rating: NOT_NULL_VALUES }))
.dataUrl((year, ice_rating) => this.urlBuilder({ year, ice_rating }))
.draw();
new StackedHistogram(this.outings, this.$refs.hiking_rating, getOutingYear, (d) => [d.hiking_rating])
.color(getHikingRatingColor)
.categoryUrl((hiking_rating) => this.urlBuilder({ hiking_rating }))
.xUrl((year) => this.urlBuilder({ year, hiking_rating: NOT_NULL_VALUES }))
.dataUrl((year, hiking_rating) => this.urlBuilder({ year, hiking_rating }))
.draw();
},
},
Expand Down
12 changes: 8 additions & 4 deletions src/views/portals/outings-stats/OutingsStatsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
:class="{ 'is-active': activeTab === activity }"
@click="activeTab = activity"
>
<a> {{ $gettext(activity, 'activities') }} ({{ outings[activity].length }}) </a>
<a>
<span v-if="activity">{{ $gettext(activity, 'activities') }}</span>
<span v-else v-translate>All</span>
<span>({{ outings[activity].length }})</span>
</a>
</li>
</ul>
</div>

<outings-stats-part v-if="outings[activeTab]" :outings="outings[activeTab]" />
<outings-stats-part v-if="outings[activeTab]" :outings="outings[activeTab]" :activity="activeTab" />
</div>
</template>

Expand All @@ -45,7 +49,7 @@ export default {
promise: null,
outings: {},
loadingPercentage: 0,
activeTab: 'all',
activeTab: '', // empty string means all
};
},
Expand All @@ -70,7 +74,7 @@ export default {
compute(outings) {
this.promise = null;
this.outings = { all: outings };
this.outings = { '': outings };
for (let activity of constants.activities) {
const result = [];
Expand Down
56 changes: 45 additions & 11 deletions src/views/portals/outings-stats/outings-stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class Histogram {
this._xTickLabel = (value) => value;
this._yTickLabel = (value) => value;
this._getY = () => 1;
this._xUrl = null;

// set the dimensions and margins of the graph
this._margin = { top: 10, right: 30, bottom: 50, left: 40 };
Expand Down Expand Up @@ -38,6 +39,11 @@ export class Histogram {
return this;
}

xUrl(xUrlGetter) {
this._xUrl = xUrlGetter;
return this;
}

yTickLabel(yTickLabelGetter) {
this._yTickLabel = yTickLabelGetter;
return this;
Expand Down Expand Up @@ -92,16 +98,27 @@ export class Histogram {
.append('g')
.attr('transform', 'translate(' + this._margin.left + ',' + this._margin.top + ')');

this._svg
const axisBottom = this._svg
.append('g')
.attr('transform', 'translate(0,' + this._height + ')')
.call(d3.axisBottom(this._xScale).tickSize(0).tickFormat(this._xTickLabel))
.call(d3.axisBottom(this._xScale).tickSize(0).tickFormat(this._xTickLabel));

axisBottom
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '0.5em')
.attr('dy', '1.5em')
.attr('transform', 'rotate(-35)');

if (this._xUrl) {
const xUrl = this._xUrl;

axisBottom.selectAll('text').each(function (d) {
const anchor = d3.select(this.parentNode).append('a').attr('xlink:href', xUrl(d));
anchor.node().appendChild(this);
});
}

this._svg.append('g').call(d3.axisLeft(this._yScale).tickFormat(this._yTickLabel));

this.drawRectangles();
Expand Down Expand Up @@ -133,6 +150,8 @@ export class StackedHistogram extends Histogram {
this._color = () => '#F93';
this._categoryComparator = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
this._categories = null;
this._categoryUrl = null;
this._dataUrl = null;
}

_computeValues() {
Expand Down Expand Up @@ -183,6 +202,11 @@ export class StackedHistogram extends Histogram {
this._values = Object.values(values);
}

categoryUrl(_categoryUrlGetter) {
this._categoryUrl = _categoryUrlGetter;
return this;
}

categoryLabel(categoryLabelGetter) {
this._getCategoryLabel = categoryLabelGetter;
return this;
Expand All @@ -202,6 +226,11 @@ export class StackedHistogram extends Histogram {
return this;
}

dataUrl(dataUrlGetter) {
this._dataUrl = dataUrlGetter;
return this;
}

drawLegend() {
// Add one dot in the legend for each name.
var size = 8;
Expand Down Expand Up @@ -237,10 +266,13 @@ export class StackedHistogram extends Histogram {
.attr('height', size)
.style('fill', (d) => colors[d]);

legend
.selectAll('mylabels')
.data(categories)
.enter()
let labels = legend.selectAll('mylabels').data(categories).enter();

if (this._categoryUrl) {
labels = labels.append('a').attr('xlink:href', this._categoryUrl);
}

labels
.append('text')
.attr('x', size * 1.2)
.attr('y', (d, i) => i * (size + 5) + size / 2)
Expand All @@ -259,12 +291,14 @@ export class StackedHistogram extends Histogram {
}

drawRectangles() {
this._svg
.selectAll('.bar')
.data(this._values)
.enter()
let rectangles = this._svg.selectAll('.bar').data(this._values).enter();

if (this._dataUrl) {
rectangles = rectangles.append('a').attr('xlink:href', (value) => this._dataUrl(value.x, value.category));
}

rectangles
.append('rect')
.attr('class', 'bar')
.attr('x', (d) => this._xScale(d.x))
.attr('width', this._xScale.bandwidth())
.attr('y', (d) => this._yScale(d.yUp))
Expand Down

0 comments on commit 9f526f7

Please sign in to comment.