Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions client/db/frequency-list/subcategory.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
<option value="all" selected>All questions</option>
</select>
</div>
<div class="col-12 mb-3">
Year range: <span id="min-year-label"></span> - <span id="max-year-label"></span>
<div class="ui-slider d-flex align-items-center" id="year-slider">
<span class="ui-slider-handle" id="min-year-handle"></span>
<span class="ui-slider-handle" id="max-year-handle"></span>
</div>
</div>
</form>
<table class="table table-hover">
<thead>
Expand Down
28 changes: 22 additions & 6 deletions client/db/frequency-list/subcategory.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import DifficultyDropdown from '../../scripts/components/DifficultyDropdown.jsx';
import { getDropdownValues } from '../../scripts/utilities/dropdown-checklist.js';
import filterParams from '../../scripts/utilities/filter-params.js';
import { DIFFICULTIES } from '../../../quizbowl/constants.js';
import { DIFFICULTIES, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../../../quizbowl/constants.js';
import { setYear, addSliderEventListeners } from '../../play/year-slider.js';

let difficulties = DIFFICULTIES;
let limit = 50;
let minYear = DEFAULT_MIN_YEAR;
let maxYear = DEFAULT_MAX_YEAR;
let questionType = 'all';
const searchParams = new URLSearchParams(window.location.search);
const isAlternate = searchParams.get('alternate') === 'true';
Expand All @@ -14,14 +17,14 @@ const subcategory = titleCase(searchParams.keys().next().value);
function difficultyDropdownListener () {
difficulties = getDropdownValues('difficulties');
if (difficulties.length === 0) { difficulties = DIFFICULTIES; }
updateFrequencyListDisplay(difficulties, limit, questionType);
updateFrequencyListDisplay(difficulties, limit, minYear, maxYear, questionType);
}

function titleCase (name) {
return name.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}

function updateFrequencyListDisplay (difficulties, limit, questionType) {
function updateFrequencyListDisplay (difficulties, limit, minYear, maxYear, questionType) {
const table = document.getElementById('frequency-list');
table.innerHTML = '';

Expand All @@ -30,6 +33,8 @@ function updateFrequencyListDisplay (difficulties, limit, questionType) {
const params = {
difficulties,
limit,
minYear,
maxYear,
questionType,
[isCategory ? 'category' : isAlternate ? 'alternateSubcategory' : 'subcategory']: subcategory
};
Expand All @@ -55,7 +60,7 @@ function updateFrequencyListDisplay (difficulties, limit, questionType) {
document.getElementById('limit-select').addEventListener('change', event => {
limit = event.target.value;
document.getElementById('limit').textContent = limit;
updateFrequencyListDisplay(difficulties, limit, questionType);
updateFrequencyListDisplay(difficulties, limit, minYear, maxYear, questionType);
});

document.getElementById('question-type-select').addEventListener('change', event => {
Expand All @@ -71,11 +76,22 @@ document.getElementById('question-type-select').addEventListener('change', event
document.getElementById('question-type').textContent = 'questions';
break;
}
updateFrequencyListDisplay(difficulties, limit, questionType);
updateFrequencyListDisplay(difficulties, limit, minYear, maxYear, questionType);
});

addSliderEventListeners((year, which) => {
if (which === 'min-year') {
minYear = year;
setYear(minYear, 'min-year');
} else {
maxYear = year;
setYear(maxYear, 'max-year');
}
updateFrequencyListDisplay(difficulties, limit, minYear, maxYear, questionType);
});

document.getElementById('subcategory-name').textContent = subcategory;
updateFrequencyListDisplay(difficulties, limit, questionType);
updateFrequencyListDisplay(difficulties, limit, minYear, maxYear, questionType);

const root = ReactDOM.createRoot(document.getElementById('difficulty-dropdown-root'));
root.render(<DifficultyDropdown onChange={difficultyDropdownListener} />);
4 changes: 2 additions & 2 deletions client/play/year-slider.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ function setYear (year, which) {
year = clip(year, GLOBAL_MIN_YEAR, GLOBAL_MAX_YEAR);
if (which === 'min-year') {
const maxYear = parseInt(document.getElementById('max-year-label').textContent);
year = Math.min(year, maxYear);
year = Math.min(year, isNaN(maxYear) ? GLOBAL_MAX_YEAR : maxYear);
} else if (which === 'max-year') {
const minYear = parseInt(document.getElementById('min-year-label').textContent);
year = Math.max(year, minYear);
year = Math.max(year, isNaN(minYear) ? GLOBAL_MIN_YEAR : minYear);
}

const handle = document.getElementById(`${which}-handle`);
Expand Down
18 changes: 18 additions & 0 deletions client/tools/api-docs/frequency-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@ <h4 class="mt-4 md-2" id="parameters">Parameters</h4>
</ul>
</div>
</li>
<li class="list-group-item">
<div>
<code>minYear</code><code class="text-muted">: number</code>
<code class="text-muted float-end fw-semibold">default: <span class="default-min-year"></span></code>
</div>
<div>
The minimum year (inclusive) to filter for.
</div>
</li>
<li class="list-group-item">
<div>
<code>maxYear</code><code class="text-muted">: number</code>
<code class="text-muted float-end fw-semibold">default: <span class="default-max-year"></span></code>
</div>
<div>
The maximum year (inclusive) to filter for. If <code>undefined</code>, no upper bound is applied.
</div>
</li>
</ul>

<a href="#returns" class="text-body">
Expand Down
11 changes: 10 additions & 1 deletion database/qbreader/get-frequency-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ import mergeTwoSortedArrays from '../../server/merge-two-sorted-arrays.js';
* @param {string} params.alternateSubcategory The alternate subcategory to get the frequency list for.
* @param {number[]} [params.difficulties] An array of difficulties to include.
* @param {number} [params.limit=50] The maximum number of answers to return.
* @param {number} [params.minYear] The minimum year to include.
* @param {number} [params.maxYear] The maximum year to include.
* @param {'tossup' | 'bonus' | 'all'} [params.questionType] The type of question to include.
* @returns {Promise<{ answer: string, count: number }[]>} The frequency list.
*/
export default async function getFrequencyList ({ alternateSubcategory, category, subcategory, difficulties, limit, questionType }) {
export default async function getFrequencyList ({ alternateSubcategory, category, subcategory, difficulties, limit, minYear, maxYear, questionType }) {
if (!category && !subcategory && !alternateSubcategory) { return []; }

const matchDocument = { difficulty: { $in: difficulties } };
if (category) { matchDocument.category = category; }
if (subcategory) { matchDocument.subcategory = subcategory; }
if (alternateSubcategory) { matchDocument.alternate_subcategory = alternateSubcategory; }
if (minYear && maxYear) {
matchDocument['set.year'] = { $gte: minYear, $lte: maxYear };
} else if (minYear) {
matchDocument['set.year'] = { $gte: minYear };
} else if (maxYear) {
matchDocument['set.year'] = { $lte: maxYear };
}

const tossupAggregation = [
{ $match: matchDocument },
Expand Down
25 changes: 22 additions & 3 deletions routes/api/frequency-list.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import getFrequencyList from '../../database/qbreader/get-frequency-list.js';
import { DIFFICULTIES } from '../../quizbowl/constants.js';
import { DEFAULT_MAX_YEAR, DEFAULT_MIN_YEAR, DIFFICULTIES } from '../../quizbowl/constants.js';

import { Router } from 'express';

Expand All @@ -15,17 +15,30 @@ const router = Router();
* @param {string} [params.subcategory] - The subcategory to get the frequency list for, if any.
* @param {number[] | string[] | number | string} [params.difficulties] - The difficulty levels to include in the frequency list. Can be an array of numbers or a comma-separated string of numbers. Default is all difficulties.
* @param {number | string} [params.limit=50] - The maximum number of answers to return. Must be a number or a string representation of a number. Default is 50.
* @param {number | string} [params.minYear] - The minimum year to include. Default is `DEFAULT_MIN_YEAR`.
* @param {number | string} [params.maxYear] - The maximum year to include. Default is `DEFAULT_MAX_YEAR`.
* @param {'tossup' | 'bonus' | 'all'} [params.questionType] - The type of question to include. Default is 'all'.
* @returns {{
* alternateSubcategory: string | undefined,
* category: string | undefined,
* difficulties: number[],
* limit: number,
* minYear: number | undefined,
* maxYear: number | undefined,
* questionType: 'tossup' | 'bonus' | 'all',
* subcategory: string | undefined
* } | null} The validated parameters, or `null` if the parameters are invalid.
*/
function validateParams ({ alternateSubcategory, category, subcategory, difficulties = DIFFICULTIES, limit = 50, questionType = 'all' }) {
function validateParams ({
alternateSubcategory,
category,
subcategory,
difficulties = DIFFICULTIES,
limit = 50,
minYear = DEFAULT_MIN_YEAR,
maxYear = DEFAULT_MAX_YEAR,
questionType = 'all'
}) {
if (typeof alternateSubcategory !== 'string' && alternateSubcategory !== undefined) {
return null;
}
Expand Down Expand Up @@ -54,7 +67,13 @@ function validateParams ({ alternateSubcategory, category, subcategory, difficul
return null;
}

return { alternateSubcategory, category, difficulties, limit, questionType, subcategory };
minYear = parseInt(minYear);
if (isNaN(minYear)) { minYear = DEFAULT_MIN_YEAR; }

maxYear = parseInt(maxYear);
if (isNaN(maxYear)) { maxYear = DEFAULT_MAX_YEAR; }

return { alternateSubcategory, category, difficulties, limit, minYear, maxYear, questionType, subcategory };
}

router.use('/', async (req, res) => {
Expand Down
Loading