diff --git a/src/Search/searchActions.js b/src/Search/searchActions.js index d5a655ee..e1ba730c 100644 --- a/src/Search/searchActions.js +++ b/src/Search/searchActions.js @@ -108,6 +108,26 @@ function handleSearch(params, inputValue, scope) { p_value: { min: '', max: '' }, }; } + // insert 'is_meta" field to fields array if ktype is 'metab' + // and delete 'protein_name' field if it exists + if (params.ktype === 'metab') { + if (!params.fields.includes('is_meta')) { + params.fields = ['is_meta', ...params.fields]; + } + if (params.fields.includes('protein_name')) { + const index = params.fields.indexOf('protein_name'); + params.fields.splice(index, 1); + } + } + // delete 'is_meta' flag from fields array (if it exists) + // if ktype is 'protein' or 'gene' + if (params.ktype === 'protein' || params.ktype === 'gene') { + if (params.fields.includes('is_meta')) { + const index = params.fields.indexOf('is_meta'); + params.fields.splice(index, 1); + } + } + return (dispatch) => { dispatch(searchSubmit(params, scope)); return axios diff --git a/src/Search/searchPage.jsx b/src/Search/searchPage.jsx index eef719ab..b3c6592b 100644 --- a/src/Search/searchPage.jsx +++ b/src/Search/searchPage.jsx @@ -20,6 +20,7 @@ import IconSet from '../lib/iconSet'; import { trackEvent } from '../GoogleAnalytics/googleAnalytics'; import { genes } from '../data/genes'; import { metabolites } from '../data/metabolites'; +import { proteins } from '../data/proteins'; import searchStructuredData from '../lib/searchStructuredData/search'; import UserSurveyModal from '../UserSurvey/userSurveyModal'; @@ -110,50 +111,61 @@ export function SearchPage({ return null; } + // get options based on selected search context + function getOptions() { + switch (searchParams.ktype) { + case 'gene': + return genes; + case 'metab': + return metabolites; + case 'protein': + return proteins; + default: + return []; + } + } + // render placeholder text in primary search input field function renderPlaceholder() { if (searchParams.ktype === 'protein') { - return 'Example: NP_001000006.1, NP_001001508.2, NP_001005898.3'; + return 'Example: "atpase inhibitor, mitochondrial", "global ischemia-induced protein 11"'; } if (searchParams.ktype === 'metab') { - return 'Example: 8,9-EpETrE, C18:1 LPC plasmalogen B'; + return 'Example: "amino acids and peptides", "c10:2 carnitine"'; } - return 'Example: BRD2, SMAD3, ID1'; + return 'Example: brd2, smad3, vegfa'; } const inputEl = document.querySelector('.rbt-input-main'); - // FIXME: transform react-bootstrap-typeahead state from array to string + // Transform input values + // Keep react-bootstrap-typeahead state array as is + // Convert manually entered gene/protein/metabolite string input to array function formatSearchInput() { const newArr = []; + // react-bootstrap-typeahead state array has values if (multiSelections.length) { multiSelections.forEach((item) => newArr.push(item.id)); - return newArr.join(', '); + return newArr; } - // Handle manually entered gene/metabolite input + // Handle manually entered gene/protein/metabolite string input + // convert formatted string to array if (inputEl.value && inputEl.value.length) { - const str = inputEl.value; - if (searchParams.ktype === 'gene') { - const arr = str.split(',').map((s) => s.trim()); - return arr.join(', '); - } - return str; + const inputStr = inputEl.value; + // Match terms enclosed in double quotes or not containing commas + const terms = inputStr.match(/("[^"]+"|[^, ]+)/g); + // Remove double quotes from terms that are enclosed and trim any extra spaces + return terms.map((term) => term.replace(/"/g, '').trim()); } - return ''; + return newArr; } // Clear manually entered gene/protein/metabolite input - function clearGeneInput(ktype) { - const inputElProtein = document.querySelector('.search-input-kype'); - - if (ktype && ktype === 'protein') { - if (inputElProtein && inputElProtein.value && inputElProtein.value.length) { - inputElProtein.value = ''; - } - } else if (inputEl && inputEl.value && inputEl.value.length) { + const clearSearchTermInput = () => { + if (inputEl && inputEl.value && inputEl.value.length) { inputRef.current.clear(); } - } + }; return (
@@ -167,23 +179,42 @@ export function SearchPage({
-
+
- Search by gene symbol, protein ID or metabolite name to examine the + Search by gene symbol, protein name or metabolite name to examine the timewise endurance training response over 8 weeks of training in - young adult rats. - {' '} - - Multiple search terms MUST be separated by comma and space. - Examples: "NP_001000006.1, NP_001001508.2, NP_001005898.3" or - "8,9-EpETrE, C18:1 LPC plasmalogen B". - - {' '} + young adult rats. To ensure the best search results, please use the + following guidelines: +
    +
  1. + Use + {' '} + + auto-suggested search terms + + {' '} + by typing the first few + characters of the gene symbol, protein or metabolite names. +
  2. +
  3. + Separate multiple search terms using a comma followed by a space. For example: + {' '} + brd2, smad3, vegfa +
  4. +
  5. + Use double quotes to enclose search terms containing commas, + spaces or commas followed by spaces. For example: + {' '} + "tca acids", "8,9-epetre", "coa(3:0, 3-oh)" +
  6. +
+

The endurance trained young adult rats dataset is made available under the {' '} CC BY 4.0 license . +

@@ -191,54 +222,28 @@ export function SearchPage({ changeParam={changeParam} ktype={searchParams.ktype} resetSearch={resetSearch} + clearInput={clearSearchTermInput} setMultiSelections={setMultiSelections} inputEl={inputEl} />
- {/* - changeParam('keys', e.target.value)} - /> - */}
pest_control_rodent
- {searchParams.ktype === 'gene' || - searchParams.ktype === 'metab' ? ( - - ) : null} - {searchParams.ktype === 'protein' && ( - changeParam('keys', e.target.value)} - /> - )} +
{ - clearGeneInput( - searchParams.ktype === 'protein' ? 'protein' : null - ); + clearSearchTermInput(); resetSearch('all'); setMultiSelections([]); }} @@ -472,6 +475,7 @@ function RadioButton({ changeParam, ktype, resetSearch, + clearInput, setMultiSelections, inputEl, }) { @@ -479,22 +483,23 @@ function RadioButton({ { keyType: 'gene', id: 'inlineRadioGene', - label: 'Gene Symbol', + label: 'Gene symbol', }, { keyType: 'protein', id: 'inlineRadioProtein', - label: 'Protein ID', + label: 'Protein name', }, { keyType: 'metab', id: 'inlineRadioMetab', - label: 'Metabolite', + label: 'Metabolite name', }, ]; const handleRadioChange = (e) => { resetSearch('all'); + clearInput(); setMultiSelections([]); changeParam('ktype', e.target.value); if (inputEl && inputEl.value && inputEl.value.length) { diff --git a/src/Search/searchReducer.js b/src/Search/searchReducer.js index 80f83709..a182bc75 100644 --- a/src/Search/searchReducer.js +++ b/src/Search/searchReducer.js @@ -14,7 +14,7 @@ export const defaultSearchState = { searchResults: {}, searchParams: { ktype: 'gene', - keys: '', + keys: [], omics: 'all', analysis: 'all', filters: { @@ -42,9 +42,11 @@ export const defaultSearchState = { 'p_value_female', ], unique_fields: ['tissue', 'assay', 'sex', 'comparison_group'], - size: 25000, + size: 10000, start: 0, save: false, + convert_assay_code: 1, + convert_tissue_code: 1, }, scope: 'all', searching: false, @@ -121,6 +123,7 @@ export function SearchReducer(state = { ...defaultSearchState }, action) { fields, unique_fields, size, + start, } = action.params; return { ...state, @@ -134,8 +137,11 @@ export function SearchReducer(state = { ...defaultSearchState }, action) { fields, unique_fields, size, + start, debug: true, save: false, + convert_assay_code: 1, + convert_tissue_code: 1, }, scope: action.scope, searching: true, @@ -157,9 +163,9 @@ export function SearchReducer(state = { ...defaultSearchState }, action) { searchResults: action.searchResults.message || action.searchResults.errors ? { - errors: + errors: action.searchResults.message || action.searchResults.errors, - } + } : action.searchResults, searching: false, hasResultFilters: @@ -189,7 +195,7 @@ export function SearchReducer(state = { ...defaultSearchState }, action) { } const defaultParams = { ...defaultSearchState.searchParams }; - defaultParams.keys = ''; + defaultParams.keys = []; defaultParams.filters = { tissue: [], assay: [], @@ -229,10 +235,10 @@ export function SearchReducer(state = { ...defaultSearchState }, action) { downloadResults: action.downloadResults.message || action.downloadResults.errors ? { - errors: - action.downloadResults.message || - action.downloadResults.errors, - } + errors: + action.downloadResults.message + || action.downloadResults.errors, + } : action.downloadResults, downloading: false, }; diff --git a/src/Search/sharedlib.jsx b/src/Search/sharedlib.jsx index bad561b3..f88b785b 100644 --- a/src/Search/sharedlib.jsx +++ b/src/Search/sharedlib.jsx @@ -2,16 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Tooltip } from 'react-tooltip'; import roundNumbers from '../lib/utils/roundNumbers'; -import { - tissueList, - assayList, - sexList, - timepointList, -} from '../lib/searchFilters'; +import { sexList, timepointList } from '../lib/searchFilters'; export const searchParamsDefaultProps = { ktype: 'gene', - keys: '', + keys: [], omics: 'all', analysis: 'all', filters: { @@ -39,15 +34,17 @@ export const searchParamsDefaultProps = { 'p_value_female', ], unique_fields: ['tissue', 'assay', 'sex', 'comparison_group'], - size: 25000, + size: 10000, start: 0, debug: true, save: false, + convert_assay_code: 1, + convert_tissue_code: 1, }; export const searchParamsPropType = { ktype: PropTypes.string, - keys: PropTypes.string, + keys: PropTypes.arrayOf(PropTypes.string), omics: PropTypes.string, analysis: PropTypes.string, filters: PropTypes.shape({ @@ -74,6 +71,8 @@ export const searchParamsPropType = { start: PropTypes.number, debug: PropTypes.bool, save: PropTypes.bool, + convert_assay_code: PropTypes.number, + convert_tissue_code: PropTypes.number, }; /** @@ -82,6 +81,7 @@ export const searchParamsPropType = { */ export const timewiseResultsTablePropType = { gene_symbol: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + protein_name: PropTypes.string, metabolite_refmet: PropTypes.string, feature_ID: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), tissue: PropTypes.string, @@ -100,6 +100,7 @@ export const timewiseResultsTablePropType = { */ export const trainingResultsTablePropType = { gene_symbol: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + protein_name: PropTypes.string, metabolite_refmet: PropTypes.string, feature_ID: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), tissue: PropTypes.string, @@ -110,55 +111,30 @@ export const trainingResultsTablePropType = { p_value_female: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }; -// react-table function to filter multiple values in one column -/* -function multipleSelectFilter(rows, id, filterValue) { - return filterValue.length === 0 - ? rows - : rows.filter((row) => filterValue.includes(row.values[id])); -} -*/ - /** * column headers common to transcriptomics, proteomics, * and metabolomic timewise dea results */ -export const timewiseTableColumns = [ - { - Header: 'Gene', - accessor: 'gene_symbol', - }, +const commonTimewiseColumns = [ { Header: 'Feature ID', accessor: 'feature_ID', }, - /* - { - Header: 'Omic', - accessor: 'omic.raw', - filter: multipleSelectFilter, - }, - */ { Header: 'Tissue', accessor: 'tissue', - // filter: 'exactText', }, { Header: 'Assay', accessor: 'assay', - // filter: 'exactText', }, - { Header: 'Sex', accessor: 'sex', - // filter: 'exactText', }, { Header: 'Timepoint', accessor: 'comparison_group', - // filter: multipleSelectFilter, }, { Header: 'logFC', @@ -179,7 +155,6 @@ export const timewiseTableColumns = [ ), accessor: 'p_value', sortType: 'basic', - // filter: 'between', }, { Header: () => ( @@ -195,7 +170,6 @@ export const timewiseTableColumns = [ ), accessor: 'adj_p_value', sortType: 'basic', - // filter: 'between', }, { Header: () => ( @@ -214,6 +188,22 @@ export const timewiseTableColumns = [ }, ]; +export const timewiseTableColumns = [ + { + Header: 'Gene', + accessor: 'gene_symbol', + }, + ...commonTimewiseColumns, +]; + +export const proteinTimewiseTableColumns = [ + { + Header: 'Protein', + accessor: 'protein_name', + }, + ...commonTimewiseColumns, +]; + export const metabTimewiseTableColumns = [ { Header: () => ( @@ -230,107 +220,25 @@ export const metabTimewiseTableColumns = [ accessor: 'metabolite_refmet', sortType: 'basic', }, - { - Header: 'Feature ID', - accessor: 'feature_ID', - }, - { - Header: 'Tissue', - accessor: 'tissue', - }, - { - Header: 'Assay', - accessor: 'assay', - }, - { - Header: 'Sex', - accessor: 'sex', - }, - { - Header: 'Timepoint', - accessor: 'comparison_group', - }, - { - Header: 'logFC', - accessor: 'logFC', - sortType: 'basic', - }, - { - Header: () => ( -
- P-value - - info - - - The p-value of the presented log fold change - -
- ), - accessor: 'p_value', - sortType: 'basic', - }, - { - Header: () => ( -
- Adj p-value - - info - - - The FDR adjusted p-value of the presented log-fold change - -
- ), - accessor: 'adj_p_value', - sortType: 'basic', - }, - { - Header: () => ( -
- Selection FDR - - info - - - Cross-tissue, IHW FDR adjusted p-value - -
- ), - accessor: 'selection_fdr', - sortType: 'basic', - }, + ...commonTimewiseColumns, ]; /** * column headers common to transcriptomics, proteomics, * and metabolomic training dea results */ -export const trainingTableColumns = [ - { - Header: 'Gene', - accessor: 'gene_symbol', - }, +const commonTrainingColumns = [ { Header: 'Feature ID', accessor: 'feature_ID', }, - /* - { - Header: 'Omic', - accessor: 'omic.raw', - filter: multipleSelectFilter, - }, - */ { Header: 'Tissue', accessor: 'tissue', - // filter: 'exactText', }, { Header: 'Assay', accessor: 'assay', - // filter: multipleSelectFilter, }, { Header: () => ( @@ -394,6 +302,22 @@ export const trainingTableColumns = [ }, ]; +export const trainingTableColumns = [ + { + Header: 'Gene', + accessor: 'gene_symbol', + }, + ...commonTrainingColumns, +]; + +export const proteinTrainingTableColumns = [ + { + Header: 'Protein', + accessor: 'protein_name', + }, + ...commonTrainingColumns, +]; + export const metabTrainingTableColumns = [ { Header: () => ( @@ -410,171 +334,26 @@ export const metabTrainingTableColumns = [ accessor: 'metabolite_refmet', sortType: 'basic', }, - { - Header: 'Feature ID', - accessor: 'feature_ID', - }, - { - Header: 'Tissue', - accessor: 'tissue', - }, - { - Header: 'Assay', - accessor: 'assay', - }, - { - Header: () => ( -
- P-value - - info - - - Combined p-value (males and females) - -
- ), - accessor: 'p_value', - sortType: 'basic', - }, - { - Header: () => ( -
- Adj p-value - - info - - - FDR-adjusted combined p-value - -
- ), - accessor: 'adj_p_value', - sortType: 'basic', - }, - { - Header: () => ( -
- Male p-value - - info - - - Training effect p-value, male data - -
- ), - accessor: 'p_value_male', - sortType: 'basic', - }, - { - Header: () => ( -
- Female p-value - - info - - - Training effect p-value, female data - -
- ), - accessor: 'p_value_female', - sortType: 'basic', - }, + ...commonTrainingColumns, ]; -/** - * Global filter rendering function - * common to timewise DEA results - */ -export const TimewiseGlobalFilter = ({ - preGlobalFilteredRows, - globalFilter, - setGlobalFilter, -}) => { - const count = preGlobalFilteredRows.length; - - return ( -
- - { - setGlobalFilter(e.target.value || undefined); - }} - placeholder={`${count} entries`} - /> -
- ); -}; - -TimewiseGlobalFilter.propTypes = { - preGlobalFilteredRows: PropTypes.arrayOf( - PropTypes.shape({ ...timewiseResultsTablePropType }) - ), - globalFilter: PropTypes.string, - setGlobalFilter: PropTypes.func.isRequired, -}; - -TimewiseGlobalFilter.defaultProps = { - globalFilter: '', - preGlobalFilteredRows: [], -}; - -/** - * Global filter rendering function - * common to training DEA results - */ -export const TrainingGlobalFilter = ({ - preGlobalFilteredRows, - globalFilter, - setGlobalFilter, -}) => { - const count = preGlobalFilteredRows.length; - - return ( -
- - { - setGlobalFilter(e.target.value || undefined); - }} - placeholder={`${count} entries`} - /> -
- ); -}; - -TrainingGlobalFilter.propTypes = { - preGlobalFilteredRows: PropTypes.arrayOf( - PropTypes.shape({ ...trainingResultsTablePropType }) - ), - globalFilter: PropTypes.string, - setGlobalFilter: PropTypes.func.isRequired, -}; - -TrainingGlobalFilter.defaultProps = { - globalFilter: '', - preGlobalFilteredRows: [], -}; - /** * page count and page index rendering function * common to all data qc status reports */ -export const PageIndex = ({ pageIndex, pageOptions }) => ( - - Showing Page {pageIndex + 1} of {pageOptions.length} - -); +export function PageIndex({ pageIndex, pageOptions }) { + return ( + + Showing Page + {' '} + {pageIndex + 1} + {' '} + of + {' '} + {pageOptions.length} + + ); +} PageIndex.propTypes = { pageIndex: PropTypes.number, @@ -590,26 +369,28 @@ PageIndex.defaultProps = { * page size control rendering function * common to all data qc status reports */ -export const PageSize = ({ pageSize, setPageSize, pageSizeOptions }) => ( -
- - - entries -
-); +export function PageSize({ pageSize, setPageSize, pageSizeOptions }) { + return ( +
+ + + entries +
+ ); +} PageSize.propTypes = { pageSize: PropTypes.number.isRequired, @@ -621,57 +402,62 @@ PageSize.propTypes = { * page navigation control rendering function * common to all data qc status reports */ -export const PageNavigationControl = ({ +export function PageNavigationControl({ canPreviousPage, canNextPage, previousPage, nextPage, gotoPage, pageCount, -}) => ( -
- {' '} - {' '} - {' '} - -
-); +}) { + return ( +
+ + {' '} + + {' '} + + {' '} + +
+ ); +} PageNavigationControl.propTypes = { canPreviousPage: PropTypes.bool.isRequired, @@ -694,7 +480,7 @@ export const transformData = (arr) => { const newGeneVal = item.gene_symbol; item.gene_symbol = ( @@ -728,20 +514,6 @@ export const transformData = (arr) => { ); } */ - // Transform tissue values - if (item.tissue && item.tissue.length) { - const matchedTissue = tissueList.find( - (filter) => filter.filter_value === item.tissue - ); - item.tissue = matchedTissue && matchedTissue.filter_label; - } - // Transform assay values - if (item.assay && item.assay.length) { - const matchedAssay = assayList.find( - (filter) => filter.filter_value === item.assay - ); - item.assay = matchedAssay && matchedAssay.filter_label; - } // Transform sex values if (item.sex && item.sex.length) { const matchedSex = sexList.find( diff --git a/src/Search/timewiseTable.jsx b/src/Search/timewiseTable.jsx index 8ba7f260..4579a6f1 100644 --- a/src/Search/timewiseTable.jsx +++ b/src/Search/timewiseTable.jsx @@ -11,6 +11,7 @@ import { searchParamsPropType, timewiseResultsTablePropType, timewiseTableColumns, + proteinTimewiseTableColumns, metabTimewiseTableColumns, PageIndex, PageSize, @@ -29,13 +30,16 @@ function TimewiseResultsTable({ handleSearchDownload, }) { // Define table column headers - const columns = useMemo( - () => - searchParams.ktype === 'metab' - ? metabTimewiseTableColumns - : timewiseTableColumns, - [] - ); + const columns = useMemo(() => { + switch (searchParams.ktype) { + case 'metab': + return metabTimewiseTableColumns; + case 'protein': + return proteinTimewiseTableColumns; + default: + return timewiseTableColumns; + } + }, [searchParams.ktype]); const data = useMemo(() => transformData(timewiseData), [timewiseData]); return ( ({ - text: (rows, id, filterValue) => - rows.filter((row) => { - const rowValue = row.values[id]; - return rowValue !== undefined - ? String(rowValue) - .toLowerCase() - .startsWith(String(filterValue).toLowerCase()) - : true; - }), - }), - [] - ); - +function DataTable({ + columns, + data, + searchParams, + handleSearchDownload, +}) { // Use the useTable hook to create your table configuration - const instance = useTable( - { - columns, - data, - filterTypes, - initialState: { - pageIndex: 0, - pageSize: 50, - pageCount: Math.ceil(data / 50), - }, - }, - useFilters, - useGlobalFilter, - useSortBy, - usePagination - ); // Use the state and functions returned from useTable to build your UI const { getTableProps, @@ -94,21 +72,31 @@ function DataTable({ columns, data, searchParams, handleSearchDownload }) { headerGroups, prepareRow, preGlobalFilteredRows, - setGlobalFilter, - preFilteredRows, - setFilter, - filterValue, pageOptions, pageCount, page, - state: { pageIndex, pageSize, globalFilter }, + state: { pageIndex, pageSize }, gotoPage, previousPage, nextPage, setPageSize, canPreviousPage, canNextPage, - } = instance; + } = useTable( + { + columns, + data, + initialState: { + pageIndex: 0, + pageSize: 50, + pageCount: Math.ceil(data / 50), + }, + }, + useFilters, + useGlobalFilter, + useSortBy, + usePagination, + ); // default page size options given the length of entries in the data const range = (start, stop, step = 50) => Array(Math.ceil(stop / step)).fill(start).map((x, y) => x + y * step); diff --git a/src/Search/trainingResultsTable.jsx b/src/Search/trainingResultsTable.jsx index dd4d24b1..166a5f1b 100644 --- a/src/Search/trainingResultsTable.jsx +++ b/src/Search/trainingResultsTable.jsx @@ -11,6 +11,7 @@ import { searchParamsPropType, trainingResultsTablePropType, trainingTableColumns, + proteinTrainingTableColumns, metabTrainingTableColumns, PageIndex, PageSize, @@ -29,13 +30,16 @@ function TrainingResultsTable({ handleSearchDownload, }) { // Define table column headers - const columns = useMemo( - () => - searchParams.ktype === 'metab' - ? metabTrainingTableColumns - : trainingTableColumns, - [] - ); + const columns = useMemo(() => { + switch (searchParams.ktype) { + case 'metab': + return metabTrainingTableColumns; + case 'protein': + return proteinTrainingTableColumns; + default: + return trainingTableColumns; + } + }, [searchParams.ktype]); const data = useMemo(() => transformData(trainingData), [trainingData]); return ( ({ - text: (rows, id, filterValue) => - rows.filter((row) => { - const rowValue = row.values[id]; - return rowValue !== undefined - ? String(rowValue) - .toLowerCase() - .startsWith(String(filterValue).toLowerCase()) - : true; - }), - }), - [] - ); - // Use the useTable hook to create your table configuration - const instance = useTable( - { - columns, - data, - filterTypes, - initialState: { - pageIndex: 0, - pageSize: 50, - pageCount: Math.ceil(data / 50), - }, - }, - useFilters, - useGlobalFilter, - useSortBy, - usePagination - ); // Use the state and functions returned from useTable to build your UI const { getTableProps, @@ -99,20 +72,31 @@ function TrainingDataTable({ headerGroups, prepareRow, preGlobalFilteredRows, - setGlobalFilter, - setFilter, - filterValue, pageOptions, pageCount, page, - state: { pageIndex, pageSize, globalFilter }, + state: { pageIndex, pageSize }, gotoPage, previousPage, nextPage, setPageSize, canPreviousPage, canNextPage, - } = instance; + } = useTable( + { + columns, + data, + initialState: { + pageIndex: 0, + pageSize: 50, + pageCount: Math.ceil(data / 50), + }, + }, + useFilters, + useGlobalFilter, + useSortBy, + usePagination, + ); // default page size options given the length of entries in the data const range = (start, stop, step = 50) => Array(Math.ceil(stop / step)).fill(start).map((x, y) => x + y * step);