/**
* @file JS SDK for NCI DCEG's PLCO API. 〜( ̄▽ ̄〜)
*
* @version 1.0
* @author Eric Ruan, Erika Nemeth, Lorena Sandoval, Jonas Almeida
* @copyright 2021
*/
console.log('plco.js loaded')
/* plco = {
date: new Date()
}
*/
/**
* Main global portable module.
* @namespace plco
* @property {Function} defineProperties - {@link plco.defineProperties}
* @property {Function} explorePhenotypes - {@link plco.explorePhenotypes}
* @property {Function} loadScript - {@link plco.loadScript}
* @property {Function} saveFile - {@link plco.saveFile}
*/
plco = async () => {
// plco.loadScript("https://cdn.plot.ly/plotly-latest.min.js")
// console.log("plotly.js loaded")
plco.loadScript("https://episphere.github.io/plotly/epiPlotly.js")
plco.addStyle()
}
/**
* TEMP work-around for the CORS issue, do NOT use in the final SDK.
*
* Instead, fetch the blob from the API and use a blob link just like in jmat.
*
* Modified from https://github.com/jonasalmeida/jmat.
* @param {string} url The download link.
* @returns {HTMLAnchorElement} HTMLAnchorElement.
*/
plco.saveFile = (url) => {
// TODO downloadgwas is not up yet, so i will make change later once its fixed
url = url || 'https://downloadgwas.cancer.gov/j_breast_cancer.tsv.gz'
let a = document.createElement('a')
a.href = url
a.target = '_blank'
a.click()
return a
}
/**
* Saves a JSON as a .json file.
* @param {object} contents A JS object.
* @param {string} fileName The name of the file without extensions.
* @returns {HTMLAnchorElement} HTMLAnchorElement.
*/
plco.downloadJSON = (contents, fileName = 'plot') => {
const a = document.createElement('a')
//a.href = URL.createObjectURL(new Blob([JSON.stringify(JSON.parse(contents))], {
a.href = URL.createObjectURL(new Blob([JSON.stringify(contents)], {
type: 'text/plain'
}))
a.setAttribute('download', fileName + '.json')
a.click()
return a
}
/**
* Adds a style tag containing css for some plot elements.
*/
plco.addStyle = () => {
const style = document.createElement('style')
style.innerHTML = `
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
position: absolute;
z-index: 10;
top: 50%;
left: 40%;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`
window.onload = (_) => {
document.head.appendChild(style)
}
}
plco.addDownloadLink = (id, f) => {
if (document.getElementById(id + 'download-json')) {
document.getElementById(id + 'download-json').remove()
}
const div = document.createElement('div')
div.id = id + 'download-json'
const button = document.createElement('button')
button.innerHTML = 'download json'
button.onclick = (async (_) => {
let data = await (f())
plco.downloadJSON(data)
})
const supElement = document.createElement('sup')
supElement.innerHTML = `<a target='_blank' href='https://episphere.github.io/plot/'>plot</a>`
div.appendChild(button)
div.appendChild(supElement)
document.getElementById(id).appendChild(div)
}
/**
* Adds a key-value pair as specified in `m_fields` and `o_fields` to `obj` if the key does not already exist.
* @param {object} obj An object.
* @param {object} m_fields Mandatory fields.
* @param {object} [o_fields={}] Optional fields. Same as `m_fields`, but will not add in the key-value pair if value is undefined.
*/
plco.defineProperties = (obj, m_fields, o_fields = {}) => {
Object.keys(m_fields).forEach((key) => {
if (typeof obj[key] === 'undefined') {
obj[key] = m_fields[key]
}
})
Object.keys(o_fields).forEach((key) => {
if (
typeof obj[key] === 'undefined' &&
typeof o_fields[key] !== 'undefined'
) {
obj[key] = o_fields[key]
}
})
}
/**
* Creates and attaches a new script tag to head with script src pointing at `url`.
* @param {string} url
* @param {string} host
* @returns {HTMLScriptElement} HTMLScriptElement
*/
plco.loadScript = async (url, host) => {
let s = document.createElement('script')
s.src = url
return document.head.appendChild(s)
}
// plco.plotTest = async (
// chr = 1,
// div,
// url = 'https://exploregwas-dev.cancer.gov/plco-atlas/api/\
// summary?phenotype_id=3080&sex=female&ancestry=east_asian&p_value_nlog_min=2&raw=true'
// ) => {
// let xx = await (await fetch(url)).json()
// div = div || document.createElement('div')
// let dt = xx.data.filter(x => x[4] == chr)
// trace = {
// x: dt.map(d => d[5]),
// y: dt.map(d => d[6]),
// mode: 'markers',
// type: 'scatter'
// }
// let layout = {
// title: `Chromosoome ${chr}`,
// xaxis: {
// title: 'position'
// },
// yaxis: {
// title: '-log(p)'
// }
// }
// plco.Plotly.newPlot(div, [trace], layout)
// return div
// }
// plco.typeCheckAttributes = (obj = {}) => {
// const typeKey = {
// phenotype_id: 'integer',
// raw: 'string'
// }
// Object.keys(obj).forEach((key) => {
// if (typeKey[key] === 'string' && typeof obj[key] !== 'string') {
// throw new TypeError(`${key} is not of type ${typeKey[key]}`)
// } else if (typeKey[key] === 'integer' && !Number.isInteger(obj[key])) {
// throw new TypeError(`${key} is not of type ${typeKey[key]}`)
// }
// })
// }
/**
* Provides a way to explore all the available phenotypes.
* @param {boolean} [flatten=false] If 'true', returns an array of objects instead of a tree.
* @param {boolean} [mini=false] If 'true', removes keys from the objects to provide a condensed view.
* @param {boolean} [graph=false] If 'true', returns a Plotly chart instead of an array of objects.
* @param {string} div_id Used when graph is 'true', plots a Plotly chart at that container.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns An array of objects.
*/
plco.explorePhenotypes = async (
flatten = false,
mini = false,
graph = false,
div_id,
customLayout = {},
customConfig = {},
) => {
let phenotypes_json = await plco.api.phenotypes()
if (flatten) {
let queue = []
let r = []
for (let i = 0; i < phenotypes_json.length; i++) {
let root = phenotypes_json[i]
queue.push(root)
while (true) {
let removed = queue.splice(0, 1)[0]
if (removed === undefined) break
r.push(removed)
if (removed.children === undefined) continue
else {
if (Array.isArray(removed.children)) {
for (let j = 0; j < removed.children.length; j++) {
queue.push(removed.children[j])
}
} else {
queue.push(removed.children)
}
}
}
}
phenotypes_json = r
for (let i = 0; i < phenotypes_json.length; i++) {
let obj = phenotypes_json[i]
delete obj.children
}
}
if (mini) {
let queue = []
for (let i = 0; i < phenotypes_json.length; i++) {
let root = phenotypes_json[i]
queue.push(root)
while (true) {
let removed = queue.splice(0, 1)[0]
if (removed === undefined) break
delete removed.color
delete removed.import_date
delete removed.type
delete removed.age_name
delete removed.import_count
delete removed.parent_id
if (removed.children === undefined) continue
else {
if (Array.isArray(removed.children)) {
for (let j = 0; j < removed.children.length; j++) {
queue.push(removed.children[j])
}
} else {
queue.push(removed.children)
}
}
}
}
}
if (graph) {
// https://plotly.com/javascript/sunburst-charts/
let div = document.getElementById(div_id)
if (!div) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
phenotypes_json = await plco.explorePhenotypes(true, false, false)
const data = [{
type: "sunburst",
ids: phenotypes_json.map(phenotype => phenotype.id),
labels: phenotypes_json.map(phenotype => phenotype.display_name),
parents: phenotypes_json.map(phenotype => phenotype.parent_id ? phenotype.parent_id : ''),
values: phenotypes_json.map(phenotype => phenotype.participant_count ? phenotype.participant_count : 0),
outsidetextfont: { size: 20, color: "#377eb8" },
leaf: { opacity: 0.6 },
marker: { line: { width: 2 } },
hovertext: phenotypes_json.map(phenotype => 'phenotype_id: ' + phenotype.id),
hoverinfo: 'label+text+value',
textposition: 'inside',
insidetextorientation: 'radial',
}]
const layout = {
margin: { l: 0, r: 0, b: 0, t: 0 },
width: 800,
height: 800,
sunburstcolorway: phenotypes_json.map(phenotype => phenotype.color).filter(Boolean),
...customLayout,
}
plco.Plotly.newPlot(div, data, layout, customConfig)
div.on('plotly_click', async ({ event, points }) => {
if (event.altKey) {
const part = await plco.api.participants({}, points[0].id, 'value,ancestry,sex', 0)
function helper(totalObject, cur, property) {
if (!totalObject['count_' + cur[property]] && cur[property] != null) {
const num = Number.parseInt(cur.counts)
if (isNaN(num))
totalObject['count_' + cur[property]] = 4
else
totalObject['count_' + cur[property]] = num
} else if (cur[property] != null) {
const num = Number.parseInt(cur.counts)
if (isNaN(num))
totalObject['count_' + cur[property]] = totalObject['count_' + cur[property]] + 4
else
totalObject['count_' + cur[property]] = totalObject['count_' + cur[property]] + num
}
}
function convertRowMajortoColMajor(numOfRows, numOfCols, arrays) {
let matrix = []
for (let i = 0; i < numOfCols; i++) {
matrix.push([])
for (let j = 0; j < numOfRows; j++) {
matrix[i].push(arrays[j][i])
}
}
return matrix
}
const data =
part.data.reduce((prev, cur) => {
const found = prev.find(obj => obj.value === cur.value)
if (!found) {
let addToArray = {
value: cur.value,
count: isNaN(Number.parseInt(cur.counts)) ? 4 : Number.parseInt(cur.counts),
}
helper(addToArray, cur, 'sex')
helper(addToArray, cur, 'ancestry')
prev.push(addToArray)
} else {
found.count = found.count +
(isNaN(Number.parseInt(cur.counts)) ? 4 : Number.parseInt(cur.counts))
helper(found, cur, 'sex')
helper(found, cur, 'ancestry')
}
return prev
}, [])
console.table(data)
const cellsVal = data.map(obj => Object.values(obj))
const cellsValPercent = data.map(obj => {
const newObj = {}
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
let k = keys[i]
if (k === 'count') {
const totalCount = data.reduce((total, cur) => total + cur.count, 0)
newObj[k] = Math.round(Number.parseInt(obj[k]) / Number.parseInt(totalCount) * 100) + '%'
} else if (k === 'value')
newObj[k] = obj[k]
else
newObj[k] = Math.round(Number.parseInt(obj[k]) / Number.parseInt(obj.count) * 100) + '%'
}
return Object.values(newObj)
})
const headerVal = Object.keys(data[0])
const trace = [{
type: 'table',
columnwidth: headerVal.map((_) => 150),
header: {
values: headerVal,
fill: { color: "#d3d3d3" },
}, cells: {
values: convertRowMajortoColMajor(
cellsVal.length, cellsVal[0].length || 0, cellsVal)
}
}]
const layout = {
updatemenus: [{
y: 1.0,
yanchor: 'top',
buttons: [{
method: 'restyle',
args: [{
cells: {
values: convertRowMajortoColMajor(
cellsVal.length, cellsVal[0].length || 0, cellsVal)
}
}],
label: 'Normal',
}, {
method: 'restyle',
args: [{
cells: {
values: convertRowMajortoColMajor(
cellsVal.length, cellsVal[0].length || 0, cellsValPercent)
}
}],
label: 'Percentage',
}],
}]
}
let div2 = document.getElementById(div_id + 'table')
if (!div2) {
div2 = document.createElement('div')
div2.id = div_id + 'table'
document.body.appendChild(div2)
}
div2.innerHTML = ''
plco.Plotly.newPlot(div2, trace, layout)
}
})
return phenotypes_json
}
return phenotypes_json
}
/**
* Fetches the result of the `url` from localForage if it exists else calls a HTTP request to the `url` and stores it into
* localForage.
* @param {string} url The url will be used as the key when storing its response into IndexedDB.
* @returns Url HTTP response.
*/
plco.fetch = async (url) => {
try {
const value = await plco.localForage.getItem(url)
if (!value || (value && !value.data)) {
const apiResult = await (await fetch(url)).json()
await plco.localForage.setItem(url, { data: apiResult, date: Math.floor(new Date().getTime() / 1000.0) })
return apiResult
} else if (value && value.date) {
if (Math.floor(new Date().getTime() / 1000.0) - value.date > 604800) {
const apiResult = await (await fetch(url)).json()
await plco.localForage.setItem(url, { data: apiResult, date: Math.floor(new Date().getTime() / 1000.0) })
return apiResult
} else {
return value.data
}
}
return value.data
} catch (error) {
console.error(error)
}
}
/**
* Sub-module grouping API methods.
* @memberof plco
* @namespace plco.api
* @property {Function} download - {@link plco.api.download}
* @property {Function} metadata - {@link plco.api.metadata}
* @property {Function} participants - {@link plco.api.participants}
* @property {Function} pca - {@link plco.api.pca}
* @property {Function} phenotypes - {@link plco.api.phenotypes}
* @property {Function} points - {@link plco.api.points}
* @property {Function} summary - {@link plco.api.summary}
* @property {Function} variants - {@link plco.api.variants}
* @property {Function} ping - {@link plco.api.ping}
*/
plco.api = {}
plco.api.url = 'https://exploregwas.cancer.gov/plco-atlas/api/'
/**
* Returns the status of the PLCO API.
*/
plco.api.ping = async () => {
return (await fetch(plco.api.url + 'ping')).text()
}
plco.api.get = async (cmd = "ping", parms = {}) => {
// res = await fetch(...)
// content-type = await res.blob().type
if (cmd === "ping") {
return await (await fetch(plco.api.url + 'ping')).text() === "true"
} else if (cmd === 'download') {
if (parms['get_link_only'] === 'true') {
return (
await fetch(
plco.api.url + cmd + '?' + plco.api.parms2string(parms)
)
).text()
} else {
plco.saveFile(
plco.api.url + cmd + '?' + plco.api.parms2string(parms)
)
return await new Promise((resolve) => resolve({}))
}
} else {
return await plco.fetch(plco.api.url + cmd + '?' + plco.api.parms2string(parms))
}
}
plco.api.string2parms = (
str = "phenotype_id=3080&sex=all&ancestry=east_asian&p_value_nlog_min=4"
) => {
let prm = {}
str.split('&').forEach(s => {
s = s.split('=')
prm[s[0]] = s[1]
})
return prm
}
plco.api.parms2string = (
prm = { phenotype_id: 3080, sex: "all", ancestry: "east_asian", p_value_nlog_min: 4 }
) => {
return Object.keys(prm).map(p => `${p}=${prm[p]}`).join('&')
}
/**
* Downloads the original association results in tsv.gz format.
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {integer} [phenotype_id=3080] A numeric phenotype id.
* @param {string} [get_link_only] _Optional_. If set to 'true', returns the download link instead of redirecting automatically to the file.
* @returns Results of the API call.
*/
plco.api.download = async (
parms,
phenotype_id = 3080,
get_link_only = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
}
plco.defineProperties(parms, { phenotype_id }, { get_link_only })
return await plco.api.get((cmd = 'download'), parms)
}
/**
* Retrieves metadata for phenotypes specified
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {number} [phenotype_id=3080] A phenotype id.
* @param {string} [sex=female] A sex, which may be "all", "female", or "male".
* @param {string} [ancestry=european] A character vector specifying ancestries to retrieve data for.
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @return A dataframe containing phenotype metadata
* @example
* plco.api.metadata()
* plco.api.metadata({ phenotype_id: 3080, sex: "female", ancestry: "european" })
* plco.api.metadata("phenotype_id=3080&sex=female&ancestry=european")
* plco.api.metadata([["phenotype_id",3080], ["sex","female"], ["ancestry","european"]])
* plco.api.metadata({}, 3080, "female", "european")
*/
plco.api.metadata = async (
parms,
phenotype_id = 3080,
sex = "female",
ancestry = "european",
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
sex,
ancestry
}
plco.defineProperties(parms, { phenotype_id, sex, ancestry }, { raw })
return await plco.api.get((cmd = 'metadata'), parms)
}
/**
* Retrieves aggregate counts for participants. Aggregate counts under 10 are returned as "< 10".
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {integer} [phenotype_id=2250] A numeric phenotype id.
* @param {string} [columns] _Optional_. A character vector specifying properties for which to retrieve counts for.
* Valid properties are: value, ancestry, genetic_ancestry, sex, and age.
* @param {integer} [precision] _Optional_. For continuous phenotypes, a numeric value specifying the -log10(precision)
* to which values should be rounded to.
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @returns Results of the API call.
*/
plco.api.participants = async (
parms,
phenotype_id = 2250,
columns = undefined,
precision = undefined,
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
columns: 'value',
precision: 0,
}
plco.defineProperties(parms, { phenotype_id }, { columns, precision, raw })
return await plco.api.get((cmd = 'participants'), parms)
}
/**
* Retrieve PCA coordinates for the specified phenotype and platform.
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {integer} [phenotype_id=3080] A numeric phenotype id.
* @param {string} [platform=PLCO_GSA] A character vector specifying the platform to retrieve data for.
* @param {integer} [pc_x=1] A numeric value (1-20) specifying the x axis's principal component.
* @param {integer} [pc_y=2] A numeric value (1-20) specifying the y axis's principal component.
* @param {integer} [limit] _Optional_. A numeric value to limit the number of variants returned (used for pagination).
* Capped at 1 million.
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @returns A dataframe containing pca coordinates.
* @example
* plco.api.pca()
* plco.api.pca({}, 3080, 'PLCO_GSA', 1, 1, 1000)
* plco.api.pca({phenotype_id: 3080, platform: 'PLCO_GSA', pc_x: 1, pc_y: 1, limit: 1000 })
* plco.api.pca("phenotype_id=3080&platform=PLCO_GSA&pc_x=1&pc_y=1&limit=1000")
* plco.api.pca([["phenotype_id",3080], ["platform","PLCO_GSA"], ["pc_x",1], ["pc_y",1], ["limit",1000]])
*/
plco.api.pca = async (
parms,
phenotype_id = 3080,
platform = 'PLCO_GSA',
pc_x = 1,
pc_y = 2,
limit = undefined,
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
platform,
pc_x,
pc_y,
limit: 10,
}
plco.defineProperties(parms, { phenotype_id, platform, pc_x, pc_y }, { limit, raw })
if (!Number.isInteger(parms['pc_x']) || parms['pc_x'] < 1 || parms['pc_x'] > 20) {
throw new RangeError('pc_x must be an integer between 1 and 20 inclusive.')
}
if (!Number.isInteger(parms['pc_y']) || parms['pc_y'] < 1 || parms['pc_y'] > 20) {
throw new RangeError('pc_y must be an integer between 1 and 20 inclusive.')
}
return await plco.api.get((cmd = 'pca'), parms)
}
/**
* Retrieves phenotypes
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {string} [q] _Optional_. A query term
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @return If query is specified, a list of phenotypes that contain the query term is returned. Otherwise, a tree of all phenotypes is returned.
* @example
* plco.api.phenotypes()
* plco.api.phenotypes({ q: "first_ca125_level" })
* plco.api.phenotypes("q=first_ca125_level")
* plco.api.phenotypes([["q","first_ca125_level"]])
* plco.api.phenotypes({}, "first_ca125_level")
*/
plco.api.phenotypes = async (
parms,
q = undefined,
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
}
plco.defineProperties(parms, {}, { q, raw })
return await plco.api.get(cmd = "phenotypes", parms)
}
/**
* Retrieves sampled variants suitable for visualizing a QQ plot for the specified phenotype, sex, and ancestry.
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {integer} [phenotype_id=3080] A numeric phenotype id.
* @param {string} [sex=female] A character vector specifying a sex to retrieve data for.
* @param {string} [ancestry=european] A character vector specifying ancestries to retrieve data for.
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @returns A dataframe containing variants.
*/
plco.api.points = async (
parms,
phenotype_id = 3080,
sex = 'female',
ancestry = 'european',
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
sex,
ancestry,
}
plco.defineProperties(parms, { phenotype_id, sex, ancestry }, { raw })
return await plco.api.get((cmd = 'points'), parms)
}
/**
* Retrieve variants for all chromosomes at a resolution of 400x800 bins across the whole genome and specified -log10(p) range
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {number} [phenotype_id=3080] A phenotype id.
* @param {string} [sex=female] A sex, which may be "all", "female", or "male".
* @param {string} [ancestry=european] A character vector specifying ancestries to retrieve data for.
* @param {number} [p_value_nlog_min=4] A numeric value >= 0 specifying the minimum -log10(p) for variants.
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @return A dataframe with aggregated variants.
* @example
* plco.api.summary()
* plco.api.summary({ phenotype_id: 3080, sex: "female", ancestry: "european", p_value_nlog_min: 4 })
* plco.api.summary("phenotype_id=3080&sex=female&ancestry=european&p_value_nlog_min=4")
* plco.api.summary([["phenotype_id",3080], ["sex","female"], ["ancestry","european"], ["p_value_nlog_min",4]])
* plco.api.summary({}, 3080, "female", "european", 4)
*/
plco.api.summary = async (
parms,
phenotype_id = 3080,
sex = "female",
ancestry = "european",
p_value_nlog_min = 4,
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
sex,
ancestry,
p_value_nlog_min
}
plco.defineProperties(parms, { phenotype_id, sex, ancestry, p_value_nlog_min }, { raw })
return await plco.api.get(cmd = "summary", parms)
}
/**
* Retrieve variants for specified phenotype, sex, ancestry, chromosome, and other optional fields.
* @param {object | string | Array<Array>} parms A JSON object, query string, or 2-d array containing the query parameters.
* @param {number} [phenotype_id=3080] Phenotype id(s)
* @param {string} [sex=female] A sex, which may be "all", "female", or "male".
* @param {string} [ancestry=european] An ancestry, which may be "african_american", "east_asian", or "european".
* @param {number} [chromosome=8] A chromosome number.
* @param {string} [columns] _Optional_ Properties for each variant. Default: all properties.
* @param {string} [snp] _Optional_ Snps.
* @param {number} [position] _Optional_ The exact position of the variant within a chromosome.
* @param {number} [position_min] _Optional_ The minimum chromosome position for variants.
* @param {number} [position_max] _Optional_ The maximum chromosome position for variants.
* @param {number} [p_value_nlog_min] _Optional_ The minimum -log10(p) of variants in the chromosome.
* @param {number} [p_value_nlog_max] _Optional_ The maximum -log10(p) of variants in the chromosome.
* @param {number} [p_value_min] _Optional_ The minimum p-value of variants in the chromosome.
* @param {number} [p_value_max] _Optional_ The maximum p-value of variants in the chromosome.
* @param {string} [orderBy] _Optional_ A property to order variants by. May be "id", "snp", "chromosome", "position", "p_value", or "p_value_nlog".
* @param {string} [order] _Optional_ An order in which to sort variants. May be "asc" or "desc".
* @param {number} [offset] _Optional_ The number of records by which to offset the variants (for pagination)
* @param {number} [limit] _Optional_ The maximum number of variants to return (for pagination). Highest allowed value is 1 million.
* @param {string} [raw] _Optional_. If true, returns data in an array of arrays instead of an array of objects.
* @return A dataframe with variants.
* @example
* plco.api.variants()
* plco.api.variants({ phenotype_id: 3080, sex: "female", ancestry: "european", chromosome: 8, limit: 10 })
* plco.api.variants("phenotype_id=3080&sex=female&ancestry=european&chromosome=8&limit=10")
* plco.api.variants([["phenotype_id",3080], ["sex","female"], ["ancestry","european"], ["chromosome",8], ["limit",10]])
* plco.api.variants({}, 3080, "female", "european", 8)
*/
plco.api.variants = async (
parms,
phenotype_id = 3080,
sex = "female",
ancestry = "european",
chromosome = 8,
columns = undefined,
snp = undefined,
position = undefined,
position_min = undefined,
position_max = undefined,
p_value_nlog_min = undefined,
p_value_nlog_max = undefined,
p_value_min = undefined,
p_value_max = undefined,
orderBy = undefined,
order = undefined,
offset = undefined,
limit = undefined,
raw = undefined
) => {
parms =
typeof parms === 'string'
? plco.api.string2parms(parms)
: Array.isArray(parms)
? Object.fromEntries(parms)
: parms
parms = parms || {
phenotype_id,
sex,
ancestry,
chromosome,
limit: 10
}
plco.defineProperties(
parms,
{ phenotype_id, sex, ancestry, chromosome },
{ columns, snp, position, position_min, position_max, p_value_nlog_min, p_value_nlog_max, p_value_min, p_value_max, orderBy, order, offset, limit, raw }
)
return await plco.api.get(cmd = "variants", parms)
}
/*
if(location.href.match('localhost')||location.href.match('127.0.0.1')){
let scriptHost=location.href.replace(/\/[^\/]*$/,'/')
plco.loadScript(`${scriptHost}plcoJonas.js`)
plco.loadScript(`${scriptHost}plcoLorena.js`)
}else{
plco.loadScript('https://episphere.github.io/plco/plcoJonas.js')
plco.loadScript('https://episphere.github.io/plco/plcoLorena.js')
}
*/
/*
if(typeof(define)!='undefined'){
define([
'https://cdn.plot.ly/plotly-latest.min.js',
//'https://episphere.github.io/plco/plco',
//'https://episphere.github.io/plco/plcoJonas.js',
//'https://episphere.github.io/plco/plcoLorena.js',
'https://cdnjs.cloudflare.com/ajax/libs/localforage/1.9.0/localforage.min.js'
],function(Plotly,localforage){
plco.localforage=localforage
plco.Plotly=Plotly
return plco
})
}
*/
/**
* Sub-module grouping plotting methods.
* @memberof plco
* @namespace plco.plot
* @prop {Function} manhattan - {@link plco.plot.manhattan}
* @prop {Function} manhattan2 - {@link plco.plot.manhattan2}
* @prop {Function} qq - {@link plco.plot.qq}
* @prop {Function} qq2 - {@link plco.plot.qq2}
* @prop {Function} pca - {@link plco.plot.pca}
* @prop {Function} pca2 - {@link plco.plot.pca2}
* @prop {Function} barchart - {@link plco.plot.barchart}
*/
plco.plot = async () => {
}
/**
* Generates a Plotly manhattan plot at the given div element with support for a single input.
* @param {string} div_id The id of the div element. If it does not exist, a new div will be created.
* @param {number} [phenotype_id=3080] A phenotype id.
* @param {string} [sex=female] A sex, which may be "all, "female", or "male".
* @param {string} [ancestry=european] An ancestry, which may be "african_american", "east_asian" or "european".
* @param {number} [p_value_nlog_min=2] A numeric value >= 0 specifying the minimum -log10(p) for variants.
* @param {integer} [chromosome] _Optional_ A single chromosome. If no chromosome argument is passed, then assume all chromosomes.
* @param {boolean} [to_json=false] _Optional_ If true, returns a stringified JSON object containing traces and layout.
* If false, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if 'to_json' is true.
* @example
* plco.plot.manhattan()
* plco.plot.manhattan('plot', 3080, "female", "european", 2, 18)
*/
plco.plot.manhattan = async (
div_id,
phenotype_id = 3080,
sex = 'female',
ancestry = 'european',
p_value_nlog_min = 2,
chromosome,
to_json = false,
customLayout = {},
customConfig = {},
) => {
const isValid = await plco.plot.helpers.validateInputs([{ phenotype_id, sex, ancestry }])
if (isValid.length <= 0) throw new Error('Invalid inputs, check the phenotype_id/sex/ancestry provided.')
// Set up div, in which Plotly graph may be inserted.
let div = document.getElementById(div_id)
if (div === null && !to_json) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
// Retrieve all summary data.
let inputData = await plco.api.summary({ phenotype_id, sex, ancestry, p_value_nlog_min })
// Filter summary data if chromosome number was specified, and set associated variables for later.
let chromosomeName
let numberOfChromosomes
if (chromosome) {
inputData = inputData.data.filter(x => x.chromosome == "" + chromosome)
chromosomeName = 'Chromosome ' + chromosome
numberOfChromosomes = 1
} else {
inputData = inputData.data
chromosomeName = 'All Chromosomes'
numberOfChromosomes = 22
}
// Retrieve rs number for all SNPs if a chromosome number was passed as an argument.
let rsNumbers = []
if (numberOfChromosomes == 1) {
let rsNumbers_allData = await plco.api.variants(
{ p_value_nlog_min, orderBy: 'id', order: 'asc' }, phenotype_id, sex, ancestry, chromosome)
rsNumbers_allData.data.map(x => rsNumbers.push({
snp: x.snp, position_abs: x.position, p_value_nlog: x.p_value_nlog
}))
}
// Set up traces
let traces = []
let chromosomeTraces = []
let currentChromosome
let largestY = p_value_nlog_min
for (i = 1; i <= numberOfChromosomes; i++) {
if (numberOfChromosomes == 1) {
currentChromosome = chromosome
} else {
currentChromosome = i
}
if (numberOfChromosomes == 1) {
currentChromosomeData = rsNumbers
} else {
currentChromosomeData = inputData.filter(x => x.chromosome == "" + currentChromosome)
}
const traceInfo = {
x: currentChromosomeData.map(x => parseInt(x.position_abs)),
y: currentChromosomeData.map(x => parseFloat(x.p_value_nlog)),
mode: 'markers',
type: 'scattergl',
marker: {
opacity: 0.65,
size: 5
},
name: 'Chromosome ' + currentChromosome, // appears as legend item
hovertemplate: currentChromosomeData.map(x =>
'absolute position: ' + parseInt(x.position_abs) +
'<br>p-value: ' + Math.pow(10, -x.p_value_nlog)
)
}
traces.push(traceInfo)
largestY = traceInfo.y.reduce((largest, cur) => largest > cur ? largest : cur, largestY)
chromosomeTraces.push({
x: [traceInfo.x.reduce((smallest, cur) => cur > smallest ? smallest : cur, Number.MAX_SAFE_INTEGER)],
y: [p_value_nlog_min],
mode: 'markers',
type: 'scattergl',
marker: {
size: 0,
opacity: 0,
color: '#FFFFFF',
},
hoverinfo: 'none',
xaxis: 'x2',
showlegend: false,
name: 'C. ' + currentChromosome,
})
}
if (numberOfChromosomes == 1) {
for (i = 0; i < traces[0].hovertemplate.length; i++) {
traces[0].hovertemplate[i] += '<br>snp: ' + rsNumbers[i].snp
}
}
traces = traces.concat(chromosomeTraces)
let layout = {
title: 'SNPs in ' + chromosomeName,
xaxis: {
title: 'absolute position',
position: '0.0',
showgrid: false,
tickfont: {
color: 'black',
size: 15
},
},
xaxis2: {
title: '',
overlaying: 'x',
anchor: 'free',
position: '1.0',
tickmode: 'array',
tickvals: chromosomeTraces.map(trace => trace.x[0]),
ticktext: chromosomeTraces.map(trace => trace.name),
},
yaxis: {
title: '-log<sub>10</sub>(p)',
fixedrange: numberOfChromosomes !== 1,
},
hovermode: 'closest',
height: 700,
width: 1200,
updatemenus: [{
y: 1.2,
yanchor: 'top',
buttons: [
{
method: 'relayout',
args: ['yaxis.range', [2, largestY]],
label: '2 (default)'
},
{
method: 'relayout',
args: ['yaxis.range', [4, largestY]],
label: '4'
},
{
method: 'relayout',
args: ['yaxis.range', [6, largestY]],
label: '6'
},
{
method: 'relayout',
args: ['yaxis.range', (largestY > 8 ? [8, largestY] : [largestY, 8])],
label: '8+'
}
]
}],
colorway: ['#FF0000', '#800000'],
...customLayout,
}
let config = {
scrollZoom: true,
...customConfig
}
// experimental
const chromosomeAbsPos =
chromosomeTraces.map(trace => ({
val: trace.x[0],
chromosomeNum: Number.parseInt(trace.name.split(' ')[1])
}))
if (!to_json) {
plco.Plotly.newPlot(div, traces, layout, config)
const man2Selector = document.getElementById(div_id + 'selector')
if (man2Selector) man2Selector.remove()
const oldCheckbox = document.getElementById(div_id + 'checkbox')
if (oldCheckbox) oldCheckbox.remove()
const oldLabel = document.getElementById(div_id + 'label')
if (oldLabel) oldLabel.remove()
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.id = div_id + 'checkbox'
const label = document.createElement('label')
label.id = div_id + 'label'
label.for = div_id + 'checkbox'
label.innerHTML = 'Allow graph to fetch additional information on zoom'
div.appendChild(checkbox)
div.appendChild(label)
div.on('plotly_relayout', async (eventdata) => {
if (checkbox.checked && eventdata['xaxis2.range[0]'] && eventdata['xaxis2.range[1]']) {
const findStart = chromosomeAbsPos.find(({ val }) => val >= eventdata['xaxis2.range[0]'])
plco.plot.helpers.addLoaderDiv(div, async () => await plco.plot.manhattan(div_id, phenotype_id, sex, ancestry, p_value_nlog_min, findStart.chromosomeNum))
}
else if (checkbox.checked) {
plco.plot.helpers.addLoaderDiv(div, async () => await plco.plot.manhattan(div_id, phenotype_id, sex, ancestry, p_value_nlog_min, undefined))
} else { return }
})
if (numberOfChromosomes === 1) {
div.on('plotly_click', eventdata => {
let resSnp = eventdata.points[0].hovertemplate.split(' ')[4]
plco.plot.helpers.qqplotHoverTooltip(
div,
div_id,
eventdata.points[0].hovertemplate + '<br>Info:<br><a href="https://www.ncbi.nlm.nih.gov/snp/' +
resSnp + '">' + resSnp + '</a>',
['#9BC4DE']
)
})
}
plco.addDownloadLink(div_id,
() => plco.plot.manhattan(div_id, phenotype_id, sex, ancestry, p_value_nlog_min,
chromosome, true, customLayout, customConfig))
return div
} else {
//let tracesString = '{"traces":' + JSON.stringify(traces) + ','
//let layoutString = '"layout":' + JSON.stringify(layout) + ','
//let configString = '"config":' + JSON.stringify(config) + '}'
//return tracesString + layoutString + configString
return {
traces:traces,
layout:layout,
config:config
}
}
}
/**
* Generates a Plotly manhattan plot at the given div element with support for multiple inputs.
* @param {string} div_id The id of the div element. If it does not exist, a new div will be created.
* @param {Array} arrayOfObjects Accepts an array of objects containing the following keys: phenotype_id, sex, ancestry.
* @param {number} [p_value_nlog_min=2] A numeric value >= 0 specifying the minimum -log10(p) for variants.
* @param {number} [chromosome=undefined] _Optional_. Leave it undefined to plot all chromosomes. Else use an integer [1-22].
* @param {boolean} [to_json=false] _Optional_ If true, returns a stringified JSON object containing traces and layout.
* If false, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if 'to_json' is true.
* @example
* await plco.plot.manhattan2('plot', [{phenotype_id: 3080, ancestry: 'european', sex: 'female'},{phenotype_id: 3080, ancestry: 'east_asian', sex: 'female'}])
*/
plco.plot.manhattan2 = async (
div_id,
arrayOfObjects,
p_value_nlog_min = 2,
chromosome,
to_json = false,
customLayout = {},
customConfig = {},
) => {
const validObjects = await plco.plot.helpers.validateInputs(arrayOfObjects)
if (validObjects.length < 2) throw new Error('Incorrect number of arguments.')
let div = document.getElementById(div_id)
if (div === null && !to_json) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
let [inputData1, inputData2] = await Promise.all([
plco.api.summary(Object.assign(validObjects[0], { p_value_nlog_min })),
plco.api.summary(Object.assign(validObjects[1], { p_value_nlog_min }))
])
let chromosomeName
let numberOfChromosomes
if (chromosome) {
inputData1 = inputData1.data.filter(x => x.chromosome == "" + chromosome)
inputData2 = inputData2.data.filter(x => x.chromosome == "" + chromosome)
chromosomeName = 'Chromosome ' + chromosome
numberOfChromosomes = 1
} else {
inputData1 = inputData1.data
inputData2 = inputData2.data
chromosomeName = 'All Chromosomes'
numberOfChromosomes = 22
}
let rsNumbers1 = []
let rsNumbers2 = []
if (numberOfChromosomes === 1) {
const promises = []
validObjects.forEach((metadataObj) => {
const { phenotype_id, sex, ancestry } = metadataObj
promises.push(plco.api.variants(
{ p_value_nlog_min, orderBy: 'id', order: 'asc' }, phenotype_id, sex, ancestry, chromosome))
})
const resultsOfPromises = await Promise.all(promises) // huge bottleneck
resultsOfPromises.forEach((arr, index) => {
if (index === 0)
arr.data.forEach(x => rsNumbers1.push({
snp: x.snp, position_abs: x.position, p_value_nlog: x.p_value_nlog, chromosome: x.chromosome
}))
else
arr.data.forEach(x => rsNumbers2.push({
snp: x.snp, position_abs: x.position, p_value_nlog: x.p_value_nlog, chromosome: x.chromosome
}))
})
}
// Set up traces
let traces = []
let traces2nd = []
let currentChromosome
let maxYInTraces = p_value_nlog_min
let largestYOne = p_value_nlog_min
let largestYTwo = p_value_nlog_min
const createTrace = (inputData, isFirst, currentChromosome) => {
let index = isFirst ? 0 : 1
currentChromosomeData = inputData.filter(x => x.chromosome == "" + currentChromosome)
const traceInfo = {
x: currentChromosomeData.map(x => parseInt(x.position_abs)),
y: currentChromosomeData.map(x => parseFloat(x.p_value_nlog)),
mode: 'markers',
type: 'scattergl',
marker: {
opacity: 0.65,
size: 5,
},
name: 'Chromosome ' + currentChromosome +
('<br>' + validObjects[index].phenotype_id + '<br>' + validObjects[index].ancestry),
// appears as legend item
hovertemplate: currentChromosomeData.map(x =>
'absolute position: ' + parseInt(x.position_abs) +
'<br>p-value: ' + Math.pow(10, -x.p_value_nlog)
)
}
if (!isFirst) {
traceInfo.xaxis = 'x'
traceInfo.yaxis = 'y2'
}
if (isFirst) {
largestYOne = traceInfo.y.reduce((max, cur) => cur > max ? cur : max, largestYOne)
} else {
largestYTwo = traceInfo.y.reduce((max, cur) => cur > max ? cur : max, largestYTwo)
}
return traceInfo
}
for (i = 1; i <= numberOfChromosomes; i++) {
if (numberOfChromosomes == 1)
currentChromosome = chromosome
else
currentChromosome = i
traces.push({
...createTrace(numberOfChromosomes == 1 ? rsNumbers1 : inputData1, true, currentChromosome),
marker: {
opacity: 0.65,
size: 5,
color: i % 2 === 1 ? '#FF0000' : '#800000',
},
})
traces2nd.push({
...createTrace(numberOfChromosomes == 1 ? rsNumbers2 : inputData2, false, currentChromosome),
marker: {
opacity: 0.65,
size: 5,
color: i % 2 === 1 ? '#5A26FF' : '#1B0071',
},
})
}
traces = traces.concat(traces2nd)
if (numberOfChromosomes == 1) {
for (let tracesNum = 0; tracesNum <= 1; tracesNum++) {
for (i = 0; i < traces[tracesNum].hovertemplate.length; i++) {
if (tracesNum === 0)
traces[tracesNum].hovertemplate[i] += '<br>snp: ' + rsNumbers1[i].snp
else
traces[tracesNum].hovertemplate[i] += '<br>snp: ' + rsNumbers2[i].snp
}
}
}
maxYInTraces = largestYOne > largestYTwo ? largestYOne : largestYTwo
let layout = {
title: 'SNPs in ' + chromosomeName,
xaxis: {
title: 'absolute position',
showgrid: false,
tickfont: {
color: 'black',
size: 15
},
},
yaxis: {
title: '-log<sub>10</sub>(p)',
},
yaxis2: {
range: [maxYInTraces, p_value_nlog_min],
},
hovermode: 'closest',
height: 700,
width: 1200,
grid: {
rows: 2,
columns: 1,
subplots: [['xy2'], ['xy']],
roworder: 'bottom to top',
ygap: 0.05,
},
plot_bgcolor: '#fff',
colorway: ['#f3cec9', '#e7a4b6', '#cd7eaf', '#a262a9', '#6f4d96', '#3d3b72', '#182844'],
updatemenus: [{
y: 1.0,
yanchor: 'top',
buttons: [
{
method: 'relayout',
args: ['yaxis.range', [2, largestYOne]],
label: '2 (default)'
},
{
method: 'relayout',
args: ['yaxis.range', [4, largestYOne]],
label: '4'
},
{
method: 'relayout',
args: ['yaxis.range', [6, largestYOne]],
label: '6'
},
{
method: 'relayout',
args: ['yaxis.range', (largestYOne > 8 ? [8, largestYOne] : [largestYOne, 8])],
label: '8+'
}
]
}, {
y: 0.6,
yanchor: 'top',
buttons: [
{
method: 'relayout',
args: ['yaxis2.range', [largestYTwo, 2]],
label: '2 (default)'
},
{
method: 'relayout',
args: ['yaxis2.range', [largestYTwo, 4]],
label: '4'
},
{
method: 'relayout',
args: ['yaxis2.range', [largestYTwo, 6]],
label: '6'
},
{
method: 'relayout',
args: ['yaxis2.range', (largestYTwo > 8 ? [largestYTwo, 8] : [8, largestYTwo])],
label: '8+'
}
]
}],
...customLayout,
}
let config = {
scrollZoom: true,
responsive: true,
...customConfig,
}
if (!to_json) {
const oldCheckbox = document.getElementById(div_id + 'checkbox')
if (oldCheckbox) oldCheckbox.remove()
const oldLabel = document.getElementById(div_id + 'label')
if (oldLabel) oldLabel.remove()
plco.Plotly.newPlot(div, traces, layout, config)
let selector = document.getElementById(div_id + 'selector')
if (selector) selector.remove()
selector = document.createElement('select')
selector.id = div_id + 'selector'
let label = document.getElementById(div_id + 'label')
if (label) label.remove()
label = document.createElement('label')
label.for = div_id + 'selector'
label.id = div_id + 'label'
label.innerHTML = 'View a single chromosome'
selector.onchange = async (event) => {
label.remove()
selector.remove()
plco.plot.helpers.addLoaderDiv(div, async () =>
await plco.plot.manhattan2(div_id, arrayOfObjects, p_value_nlog_min, event.target.value,
to_json, customLayout, customConfig))
}
for (let i = 0; i <= 22; i++) {
if (i === 0) {
const optblank = document.createElement('option')
optblank.value = ''
optblank.innerHTML = ' '
selector.appendChild(optblank)
const opt = document.createElement('option')
opt.value = ''
opt.innerHTML = 'All'
selector.appendChild(opt)
} else {
const opt = document.createElement('option')
opt.value = i
opt.innerHTML = i
selector.appendChild(opt)
}
}
div.appendChild(selector)
div.appendChild(label)
if (numberOfChromosomes === 1) {
div.on('plotly_click', eventdata => {
let resSnp = eventdata.points[0].hovertemplate.split(' ')[4]
plco.plot.helpers.qqplotHoverTooltip(
div,
div_id,
eventdata.points[0].hovertemplate +
'<br>Info:<br><a href="https://www.ncbi.nlm.nih.gov/snp/' + resSnp + '">' + resSnp + '</a>',
[eventdata.points[0].data.marker.color]
)
})
}
plco.addDownloadLink(div_id,
() => plco.plot.manhattan2(div_id, arrayOfObjects, p_value_nlog_min,
chromosome, true, customLayout, customConfig))
return div
} else {
//let tracesString = '{"traces":' + JSON.stringify(traces) + ','
//let layoutString = '"layout":' + JSON.stringify(layout) + ','
//let configString = '"config":' + JSON.stringify(config) + '}'
//return tracesString + layoutString + configString
return {
traces:traces,
layout:layout,
config:config
}
}
}
/**
* Generates a Plotly quartile-quartile plot at the given div element with support for a single input.
* @param {string} div_id The id of the div element, if it does not exist, a new div will be created.
* @param {number} [phenotype_id=3080] A phenotype id.
* @param {string} [sex=female] A sex, which may be "all", "female", or "male".
* @param {string} [ancestry=east_asian] A character vector specifying ancestries to retrieve data for.
* @param {boolean} [to_json=false] _Optional_. If true, returns a stringified JSON object containing traces and layout.
* Else, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if `to_json` is true.
* @example
* await plco.plot.qq('plot', 3080, 'female', 'east_asian')
*/
plco.plot.qq = async (
div_id,
phenotype_id = 3080,
sex = 'female',
ancestry = 'east_asian',
to_json = false,
customLayout = {},
customConfig = {},
) => {
/**
* @type {Array<object>} Each object in the array has the properties defined below.
* @prop {integer} id
* @prop {integer} phenotype_id
* @prop {string} phenotype_name
* @prop {string} phenotype_display_name
* @prop {string} sex
* @prop {string} ancestry
* @prop {string} chromosome
* @prop {number} lambda_gc
* @prop {number} lambda_gc_ld_score
* @prop {integer} count
*/
const metadata = (await plco.plot.helpers.validateInputs([{ phenotype_id, sex, ancestry }]))[0]
if (metadata === undefined || metadata['count'] === null) {
throw new Error('No data found for this combination of sex and/or ancestry.')
}
/**
* @type {object} Object with the following props:
* @prop {Array<object>} data
* @prop {Array<string>} columns
*/
const points = await plco.api.points({}, phenotype_id, sex, ancestry)
let div = document.getElementById(div_id)
if (div === null && !to_json) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
const trace = {
x: points.data.map(p => p.p_value_nlog_expected),
y: points.data.map(p => p.p_value_nlog),
type: 'scattergl',
mode: 'markers',
marker: {
color: '#A71515',
size: 4,
opacity: 0.75
},
// customdata is an array containing more data on each of the individual point corresponding to indices of x
// this is useful later with the onClick event
customdata: points.data.map((point) => ({
phenotype_id,
sex,
ancestry,
variantId: point['id'],
p: Math.pow(10, -point['p_value_nlog']),
})),
text: points.data.map(point =>
'Variant Id: ' + point['id'] +
'<br>p-value: ' + Math.pow(10, -point['p_value_nlog']) +
'<br>Click to learn more.'),
hoverinfo: 'text+x+y+name',
name: `${metadata.phenotype_display_name}, ${sex}, ${ancestry}`,
}
const max = points.data.reduce(
(max, cur) => cur.p_value_nlog_expected > max ? cur.p_value_nlog_expected : max, 0)
const traceLine = {
x: [0, max],
y: [0, max],
type: 'scattergl', // scattergl seems to load faster than scatter, else no major diff
mode: 'line',
marker: {
color: '#BBB',
opacity: 0.4,
size: 0.1,
},
hoverinfo: 'none',
showlegend: false,
}
const layout = {
hoverlabel: {
bgcolor: '#FFF',
bordercolor: '#BBB',
font: {
size: 14,
color: '#212529',
}
},
width: 750,
height: 750,
title: {
text:
`\u03BB (median) = ${metadata['lambda_gc']} <b>|</b>` +
`\u03BB (LD score) = ${metadata['lambda_gc_ld_score']} <b>|</b> ` +
`Number of variants = ${metadata['count']}`,
font: {
size: 14,
color: 'black'
}
},
xaxis: {
automargin: true,
rangemode: 'tozero',
showgrid: false,
fixedrange: false,
title: {
text: '<b>Expected -log<sub>10</sub>(p)</b>',
font: {
size: 15,
color: 'black'
}
},
ticklen: 10, // Length of the tick marks on the x-axis
tickwidth: 1,
dtick: 0.5,
tickfont: {
size: 12,
color: 'black'
}
},
yaxis: {
automargin: true,
rangemode: 'tozero',
showgrid: true,
fixedrange: false,
title: {
text: '<b>Observed -log<sub>10</sub>(p)</b>',
font: {
size: 15,
color: 'black'
}
},
ticklen: 10, // Length of the tick marks on the y-axis
tickwidth: 1,
dtick: 0.5,
tickfont: {
size: 12,
color: 'black'
}
},
clickmode: 'event',
hovermode: 'closest', // When a point is hovered, it will display their (x, y) coordinate
dragmode: 'pan',
showlegend: false,
...customLayout
}
const config = {
scrollZoom: true,
displaylogo: false,
modeBarButtonsToRemove: [
'lasso2d',
'select2d',
'toggleSpikelines',
'autoScale2d',
'hoverCompareCartesian',
'hoverClosestCartesian',
],
...customConfig
}
if (!to_json) {
plco.Plotly.newPlot(div, [traceLine, trace], layout, config)
div.on('plotly_click', async (data) => {
console.log(data) // contains the custom data
// An array
const { points } = data
for (let i = 0; i < points.length; i++) {
try {
const res = await plco.api.get('variants', {
id: points[i].customdata.variantId,
phenotype_id,
sex,
ancestry,
columns: 'chromosome,position,snp',
})
const { chromosome: resChromosome, position: resPosition, snp: resSnp } = res.data[0]
let updatedText = points[i].data.text.slice()
let filteredText = updatedText[points[i].pointIndex].substring(0, updatedText[points[i].pointIndex].indexOf('Click'))
updatedText[points[i].pointIndex] = filteredText + `<br>Chromosome: ${resChromosome} <br>` +
`Position: ${resPosition} <br> SNP: ${resSnp}`
plco.plot.helpers.qqplotHoverTooltip(
div,
div_id,
filteredText + `Chromosome: ${resChromosome} <br>` +
`Position: ${resPosition} <br> SNP: ${resSnp}` +
'<br><a href="https://www.ncbi.nlm.nih.gov/snp/' + resSnp + '">' + 'Learn more at dbSNP: ' +
resSnp + '</a>',
['#d3d3d3']
)
plco.Plotly.restyle(div, { text: [updatedText] }, [1])
} catch (e) {
console.error(e)
}
}
})
plco.addDownloadLink(div_id,
() => plco.plot.qq(div_id, phenotype_id, sex, ancestry, true, customLayout, customConfig))
return div
} else {
const tracesString = '{"traces":' + JSON.stringify([traceLine, trace]) + ','
const layoutString = '"layout":' + JSON.stringify(layout) + ','
const configString = '"config":' + JSON.stringify(config) + '}'
return tracesString + layoutString + configString
/*
return {
traces:traces,
layout:layout,
config:config
}
*/
}
}
/**
* Generates a Plotly quartile-quartile plot at the given div element with support for multiple inputs.
* @param {string} div_id The id of the div element, if it does not exist, a new div will be created.
* @param {Array} arrayOfObjects Accepts an array of objects containing the following keys: phenotype_id, sex, ancestry.
* @param {boolean} [to_json=false] _Optional_. If true, returns a stringified JSON object containing traces and layout.
* Else, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if `to_json` is true.
* @example
* await plco.plot.qq2('plot', [{phenotype_id:3080, sex:'female', ancestry:'east_asian'}, {phenotype_id:3080, sex:'female', ancestry:'european'}, {phenotype_id: 3550, sex:'all', ancestry:'east_asian'}])
*/
plco.plot.qq2 = (
div_id,
arrayOfObjects = [],
to_json = false,
customLayout = {},
customConfig = {},
) => {
const promises = []
arrayOfObjects.forEach((obj) => {
const { phenotype_id, sex, ancestry } = obj
if (!phenotype_id || !sex || !ancestry) {
console.error('An object is missing mandatory fields, skipping ...')
return
} else {
promises.push(
plco.plot.qq('', phenotype_id, sex, ancestry, true)
.catch(() => {
console.error('Unable to fetch data, skipping...')
return undefined
})
)
}
})
/**
* @type {Array<object>} Each object has the following props:
* @prop {Array<object>} traces
* @prop {object} layout
*/
return Promise.all(promises)
.then((_) => _.filter(Boolean)) // Filters out all undefined and null
.then((arrayOfJsonStr) => arrayOfJsonStr.map((str) => JSON.parse(str)))
.then((arrayOfJson) => {
if (arrayOfJson.length === 0) return
const colors = ['#01A5E4', '#FFBF65', '#FF5768', '#8DD7C0', '#FF96C6']
let div = document.getElementById(div_id)
if (div === null && !to_json) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
const traces = [arrayOfJson[0].traces[0]]
arrayOfJson.forEach((obj, index) => {
traces.push({
...obj.traces[1],
marker: {
color: colors[index % colors.length],
size: 4,
opacity: 0.6
},
})
})
const layout = {
...arrayOfJson[0].layout,
showlegend: true,
width: 900,
height: 900,
hoverlabel: {
},
title: {
text: arrayOfJson.reduce((word, cur, index) =>
word + traces[index + 1].name + ' ' + cur.layout.title.text + '<br>', ''),
font: {
size: 12,
color: 'black'
}
},
...customLayout
}
const config = {
scrollZoom: true,
displaylogo: false,
modeBarButtonsToRemove: [
'lasso2d',
'select2d',
'toggleSpikelines',
'autoScale2d',
'hoverCompareCartesian',
'hoverClosestCartesian',
],
...customConfig
}
if (!to_json) {
plco.Plotly.newPlot(div, traces, layout, config)
div.on('plotly_click', async (data) => {
console.log(data) // contains the custom data
// An array
const { points } = data
for (let i = 0; i < points.length; i++) {
try {
const { variantId: id, phenotype_id, sex, ancestry } = points[i].customdata
const res = await plco.api.get('variants', {
id,
phenotype_id,
sex,
ancestry,
columns: 'chromosome,position,snp',
})
const { chromosome: resChromosome, position: resPosition, snp: resSnp } = res.data[0]
let updatedText = points[i].data.text.slice()
let filteredText = updatedText[points[i].pointIndex].substring(0, updatedText[points[i].pointIndex].indexOf('Click'))
updatedText[points[i].pointIndex] = filteredText + `<br>Chromosome: ${resChromosome} <br>` +
`Position: ${resPosition} <br> SNP: ${resSnp}`
plco.plot.helpers.qqplotHoverTooltip(
div,
div_id,
filteredText + `Chromosome: ${resChromosome} <br>` +
`Position: ${resPosition} <br> SNP: ${resSnp}` +
'<br><a href="https://www.ncbi.nlm.nih.gov/snp/' + resSnp + '">' + resSnp + '</a>',
colors,
points[i].curveNumber
)
plco.Plotly.restyle(div, { text: [updatedText] }, [points[i].curveNumber])
} catch (e) {
console.error(e)
}
}
})
plco.addDownloadLink(div_id,
() => plco.plot.qq2(div_id, arrayOfObjects, true, customLayout, customConfig))
return div
} else {
//const tracesString = '{"traces":' + JSON.stringify(traces) + ','
//const layoutString = '"layout":' + JSON.stringify(layout) + ','
//const configString = '"config":' + JSON.stringify(config) + '}'
//return tracesString + layoutString + configString
return {
traces:traces,
layout:layout,
config:config
}
}
})
}
/**
* Generates a Plotly PCA plot at the given div element with support for a single input.
* @param {string} div_id The id of the div element, if it does not exist, a new div will be created.
* @param {number} phenotype_id A phenotype id.
* @param {string} sex A sex, which may be "all", "female", or "male".
* @param {string} ancestry A character vector specifying ancestries to retrieve data for.
* @param {boolean} [to_json=false] _Optional_. If true, returns a stringified JSON object containing traces and layout.
* Else, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if `to_json` is true.
* @example
* await plco.plot.pca('plot', 3080, 'female', 'east_asian')
*/
plco.plot.pca = async (
div_id,
phenotype_id,
sex,
ancestry,
to_json = false,
customLayout = {},
customConfig = {}
) => {
return await plco.plot.pca2(div_id, [{ phenotype_id, sex, ancestry }], to_json, customLayout, customConfig)
}
/**
* Generates a Plotly PCA plot at the given div element with support for multiple inputs.
* @param {string} div_id The id of the div element, if it does not exist, a new div will be created.
* @param {Array} arrayOfObjects Accepts an array of objects containing the following keys: phenotype_id, sex, ancestry.
* @param {boolean} [to_json=false] _Optional_. If true, returns a stringified JSON object containing traces and layout.
* Else, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if `to_json` is true.
* @example
* await plco.plot.pca2('plot', [{phenotype_id: 3080, sex: 'female', ancestry: 'east_asian'}, {phenotype_id: 3080, sex: 'female', ancestry: 'european'}])
*/
plco.plot.pca2 = async (
div_id,
arrayOfObjects,
to_json = false,
customLayout = {},
customConfig = {}
) => {
let pc_x = 1
let pc_y = 2
/**
* Metadata
* @type {Array<object>} Each object in the array has the properties defined below.
* @prop {integer} id
* @prop {integer} phenotype_id
* @prop {string} phenotype_name
* @prop {string} phenotype_display_name
* @prop {string} sex
* @prop {string} ancestry
* @prop {string} chromosome
* @prop {number} lambda_gc
* @prop {number} lambda_gc_ld_score
* @prop {integer} count
*/
/**
* PCA
* @type {object}
* @prop {Array} columns
* @prop {Array} data - `data` is an array of object that has the following props:
* pc_x, pc_y, ancestry, sex, value
*/
let div = document.getElementById(div_id)
if (div === null && !to_json) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
// Other are the points that do not share the inputted ancestry or sex or value == null
// Control are the same ancestry and sex, but value == null or 0
// Cases are the same ancestry and sex, but value != null and != 0
const traces = await plco.plot.helpers.pcaHelper(arrayOfObjects, 'PLCO_GSA', 1, 2)
const layout = {
hovermode: 'closest',
dragmode: 'pan',
clickmode: 'event',
width: 800,
height: 800,
autosize: true,
xaxis: {
automargin: true,
showgrid: false,
title: {
text: `<b>PC-X ${(pc_x || '1')}</b>`,
font: {
size: 14,
color: 'black'
}
},
tick0: 0,
ticklen: 10,
tickfont: {
size: 10,
color: 'black'
}
},
yaxis: {
automargin: true,
showgrid: false,
title: {
text: `<b>PC-Y ${(pc_y || '2')}</b>`,
font: {
size: 14,
color: 'black'
}
},
tick0: 0,
ticklen: 10,
tickfont: {
size: 10,
color: 'black'
}
},
showlegend: true,
legend: {
title: {
font: {
size: 12,
color: 'grey'
}
},
itemdoubleclick: false,
orientation: 'v',
x: 0.0,
y: 1.2
},
...customLayout,
}
const config = {
scrollZoom: true,
responsive: true,
toImageButtonOptions: {
format: 'svg',
filename: 'pca_plot',
height: 1000,
width: 1000,
scale: 1
},
displaylogo: false,
modeBarButtonsToRemove: [
'select2d',
'autoScale2d',
'hoverClosestCartesian',
'hoverCompareCartesian',
'lasso2d',
'toggleSpikelines',
],
...customConfig,
}
const dropdownLayout = await plco.plot.helpers.pcaCreateDropdownLayout(arrayOfObjects, 1, 2)
if (!to_json) {
plco.Plotly.newPlot(div, traces, Object.assign(layout, dropdownLayout), config)
plco.plot.helpers.pcaGenerateXYInputs(div_id, arrayOfObjects, layout, config)
plco.addDownloadLink(div_id,
() => plco.plot.pca2(div_id, arrayOfObjects, true, customLayout, customConfig))
return div
} else {
//const tracesString = '{"traces":' + JSON.stringify(traces) + ','
//const layoutString = '"layout":' + JSON.stringify(Object.assign(layout, dropdownLayout)) + ','
//const configString = '"config":' + JSON.stringify(config) + '}'
//return tracesString + layoutString + configString
return {
traces:traces,
layout:Object.assign(layout, dropdownLayout),
config:config
}
}
}
/**
* Generates a Plotly barchart showing the breakdown of participants by gender and ancestry for that phenotype.
* @param {string} div_id The id of the div element, if it does not exist, a new div will be created.
* @param {number} phenotype_id A phenotype id.
* @param {boolean} [to_json=false] _Optional_. If true, returns a stringified JSON object containing traces and layout.
* Else, returns a div element containing the Plotly graph.
* @param {object} [customLayout={}] _Optional_. Contains Plotly supported layout key-values pair that will overwrite the default layout. Commonly overwritten values may include height and width of the graph. See: https://plotly.com/javascript/reference/layout/ for more details. Also, set `to_json` to true to see what the default layout is.
* @param {object} [customConfig={}] _Optional_. Contains Plotly supported config key-values pair that will overwrite the default config. See: https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L22-L86 for full details.
* @returns A div element or a string if `to_json` is true.
*/
plco.plot.barchart = async (
div_id,
phenotype_id,
to_json = false,
customLayout = {},
customConfig = {}
) => {
const [uniqueValues, participantData] = await Promise.all([
plco.api.participants({}, phenotype_id, 'value', 0).then(res => res.data),
plco.api.participants({}, phenotype_id, 'value,ancestry,sex', 0).then(res => res.data)
])
const traces = []
uniqueValues.forEach(({ value }, index) => {
for (let sex of ['male', 'female']) {
const filteredArray = participantData.filter(partObj =>
partObj.value === value && partObj.sex === sex && partObj.ancestry != null)
if (filteredArray.length > 0) {
if (index != 0)
traces.push({
x: filteredArray.map(partObj => partObj.ancestry),
y: filteredArray.map(partObj =>
isNaN(Number.parseInt(partObj.counts)) ? 10 : Number.parseInt(partObj.counts)),
xaxis: `x${index + 1}`,
yaxis: `y${index + 1}`,
type: 'bar',
name: value + ', ' + sex
})
else
traces.push({
x: filteredArray.map(partObj => partObj.ancestry),
y: filteredArray.map(partObj =>
isNaN(Number.parseInt(partObj.counts)) ? 10 : Number.parseInt(partObj.counts)),
type: 'bar',
name: value + ', ' + sex
})
}
}
})
const layout = {
grid: { rows: uniqueValues.length, columns: 1, pattern: 'independent' },
height: 800,
dragmode: 'pan',
barmode: 'group',
title: {
text: 'Frequency of partipicants by ancestry and sex',
font: {
size: 21,
color: 'black'
},
},
xaxis: {
showgrid: false,
title: {
text: 'Ancestry',
font: {
size: 15,
color: 'black'
},
},
},
yaxis: {
showgrid: true,
title: {
text: 'Number of participants',
font: {
size: 15,
color: 'black'
},
},
},
showlegend: true,
legend: {
x: 1,
xanchor: 'right',
y: 1
},
...customLayout,
}
for (let i = 1; i < uniqueValues.length; i++) {
layout[`xaxis${i + 1}`] = {
title: 'Ancestry',
font: {
size: 15,
color: 'black'
},
}
layout[`yaxis${i + 1}`] = {
title: 'Number of participants',
font: {
size: 15,
color: 'black'
},
}
}
const config = {
scrollZoom: true,
responsive: true,
...customConfig,
}
if (!to_json) {
let div = document.getElementById(div_id)
if (div === null && !to_json) {
div = document.createElement('div')
div.id = div_id
document.body.appendChild(div)
}
plco.Plotly.newPlot(div, traces, layout, config)
return div
} else {
//const tracesString = '{"traces":' + JSON.stringify(traces) + ','
//const layoutString = '"layout":' + JSON.stringify(layout) + ','
//const configString = '"config":' + JSON.stringify(config) + '}'
//return tracesString + layoutString + configString
return {
traces:traces,
layout:layout,
config:config
}
}
}
/**
* A collection of helper methods for plotting, not intended to be used on its own.
* @memberof plco.plot
*/
plco.plot.helpers = {}
plco.plot.helpers.addLoaderDiv = async (div, f) => {
const tempDiv = document.createElement('div')
tempDiv.classList.add('loader')
let top = div.getBoundingClientRect().top + window.pageYOffset - div.ownerDocument.documentElement.clientTop
tempDiv.style.top = `${top}px`
document.body.appendChild(tempDiv)
div.style = 'display: none;'
await (f())
tempDiv.remove()
div.style = ''
}
plco.plot.helpers.qqplotHoverTooltip = (div, div_id, text, colors, curveNumber = 1) => {
let top = div.getBoundingClientRect().top + window.pageYOffset - div.ownerDocument.documentElement.clientTop
let randint = Math.round(Math.random() * 1000)
let hoverDiv = document.getElementById(div_id + 'hoverdiv' + randint)
if (!hoverDiv) {
hoverDiv = document.createElement('div')
hoverDiv.id = div_id + 'hoverdiv' + randint
div.appendChild(hoverDiv)
}
hoverDiv.innerHTML = text
hoverDiv.style =
`position:absolute;top:${top}px;left:40%;z-index:10;background-color:${colors[(curveNumber - 1) % colors.length]};` +
`text-align:center; border: 1px solid #000; cursor:move; padding:10px; font-size: 0.7rem;`
hoverDiv.className = 'plotHoverDivs'
let closeButton = document.createElement('button')
closeButton.addEventListener('click', function () { this.parentElement.remove() })
closeButton.id = div_id + 'closeButton' + randint
closeButton.innerHTML = 'Close'
closeButton.style = 'display:block;'
hoverDiv.appendChild(closeButton)
hoverDiv.addEventListener('mousedown', e => {
down = true
startX = e.clientX // global vars
startY = e.clientY // global vars
})
hoverDiv.addEventListener('mouseup', e => {
down = false
})
hoverDiv.addEventListener('mousemove', e => {
try {
if (down) {
hoverDiv.style.left = hoverDiv.offsetLeft - (startX - e.clientX) + 'px'
hoverDiv.style.top = hoverDiv.offsetTop - (startY - e.clientY) + 'px'
startX = e.clientX // global vars
startY = e.clientY // global vars
}
} catch (e) {
down = false
}
})
}
plco.plot.helpers.validateInputs = async (
arrayOfObjects = []
) => {
const promises = []
arrayOfObjects.forEach(({ phenotype_id, sex, ancestry }) => {
if (phenotype_id && sex && ancestry) {
promises.push(
plco.api.metadata({ chromosome: 'all' }, phenotype_id, sex, ancestry)
.then(array => array[0])
.then(metadata => {
if (metadata === undefined || metadata['count'] === null) {
throw new Error('No data found for this combination of sex and/or ancestry.')
} else
return metadata
})
.catch(() => {
console.error('Unable to fetch data, skipping...')
return undefined
})
)
}
})
return (await Promise.all(promises)).filter(Boolean)
}
plco.plot.helpers.pcaGenerateTraces = async (
validArray,
platform,
pc_x,
pc_y
) => {
// [light, dark]
const colors = [['#FF626F', '#B2444D'], ['#FFD5A6', '#CCAA84'],
['#8DD7C0', '#6BA392'], ['#01A7FF', '#0085CC']]
const pcaPromises = []
validArray.forEach((obj) =>
pcaPromises.push(
plco.api.pca({}, obj.phenotype_id, platform, pc_x, pc_y)
)
)
// Pca [] should have a one-to-one correspondence with validArray []
const pcadatas = await Promise.all(pcaPromises).catch(() => [])
if (pcadatas.length === 0) return []
const baseTrace = {
type: 'scattergl',
hoverinfo: 'x+y',
showlegend: true,
mode: 'markers',
}
const traces = []
const otherTraces = []
pcadatas.forEach((item, index) => {
try {
// Create the other traces first
const others = item.data.filter(obj =>
obj.ancestry !== validArray[index].ancestry ||
obj.sex !== validArray[index].sex || obj.value === null
)
otherTraces.push({
...baseTrace,
x: others.map(obj => obj.pc_x),
y: others.map(obj => obj.pc_y),
marker: {
color: '#A6A6A6',
size: 4,
opacity: 0.35
},
name: 'Other, Count: ' + others.length
})
const controls = item.data.filter(obj =>
obj.ancestry === validArray[index].ancestry &&
obj.sex === validArray[index].sex && (obj.value === null || obj.value === 0)
)
const cases = item.data.filter(obj =>
obj.ancestry === validArray[index].ancestry &&
obj.sex === validArray[index].sex && (obj.value !== null && obj.value !== 0)
)
const controlsTrace = {
...baseTrace,
x: controls.map(obj => obj.pc_x),
y: controls.map(obj => obj.pc_y),
marker: {
color: colors[index % colors.length][1],
size: 5,
opacity: 0.65
},
name: `Controls ${validArray[index].phenotype_display_name}, ${validArray[index].ancestry}, ` +
`${validArray[index].sex[0]}, Count: ${controls.length}`
}
const casesTrace = {
...baseTrace,
x: cases.map(obj => obj.pc_x),
y: cases.map(obj => obj.pc_y),
marker: {
color: colors[index % colors.length][0],
size: 5,
opacity: 0.65
},
name: `Cases ${validArray[index].phenotype_display_name}, ${validArray[index].ancestry}, ` +
`${validArray[index].sex[0]}, Count: ${cases.length}`
}
traces.push(controlsTrace, casesTrace)
}
catch (_) {
console.error('PC_X and PC_Y cannot be equal.')
}
})
return otherTraces.concat(traces)
}
plco.plot.helpers.pcaHelper = async (
arrayOfObjects,
platform,
pc_x,
pc_y
) => {
const metadatas = await plco.plot.helpers.validateInputs(arrayOfObjects)
const traces = await plco.plot.helpers.pcaGenerateTraces(metadatas, platform, pc_x, pc_y)
return traces
}
plco.plot.helpers.pcaCreateDropdownLayout = async (validArray, pc_x, pc_y) => {
// https://plotly.com/javascript/dropdowns/
const layout = {
updatemenus: [{
y: 1.2,
yanchor: 'top',
buttons: [],
}]
}
const platforms = ['PLCO_GSA', 'PLCO_Omni5', 'PLCO_Omni25', 'PLCO_Oncoarray', 'PLCO_OmniX']
const promises = []
const metadatas = await plco.plot.helpers.validateInputs(validArray)
for (let i = 0; i < platforms.length; i++) {
promises.push(
plco.plot.helpers.pcaGenerateTraces(metadatas, platforms[i], pc_x, pc_y)
.then((traces) => {
layout.updatemenus[0].buttons.push({
method: 'restyle',
args: [{ x: traces.map(t => t.x), y: traces.map(t => t.y) }],
label: platforms[i],
})
})
)
}
await Promise.all(promises)
return layout
}
plco.plot.helpers.pcaGenerateXYInputs = (div_id, arrayOfObjects, layout, config) => {
let xSelector = document.getElementById(div_id + 'xSelector')
if (xSelector) {
xSelector.remove()
}
xSelector = document.createElement('select')
xSelector.id = div_id + 'xSelector'
xSelector.title = 'pcaSelector'
let ySelector = document.getElementById(div_id + 'ySelector')
if (ySelector) {
ySelector.remove()
}
ySelector = document.createElement('select')
ySelector.id = div_id + 'ySelector'
ySelector.title = 'pcaSelector'
let xLabel = document.getElementById(div_id + 'xLabel')
if (xLabel) {
xLabel.remove()
}
xLabel = document.createElement('label')
xLabel.for = div_id + 'xSelector'
xLabel.innerHTML = 'PC_X: '
xLabel.id = div_id + 'xLabel'
let yLabel = document.getElementById(div_id + 'yLabel')
if (yLabel) {
yLabel.remove()
}
yLabel = document.createElement('label')
yLabel.for = div_id + 'ySelector'
yLabel.innerHTML = 'PC_Y: '
yLabel.id = div_id + 'yLabel'
for (let i = 1; i <= 20; i++) {
const opt = document.createElement('option')
opt.text = i
xSelector.appendChild(opt)
if (i === 1) continue
const opt2 = document.createElement('option')
opt2.text = i
ySelector.appendChild(opt2)
}
const opt3 = document.createElement('option')
opt3.text = 1
ySelector.appendChild(opt3)
async function eventListener() {
const xVal = Number.parseFloat(xSelector.value)
const yVal = Number.parseFloat(ySelector.value)
const traces = await plco.plot.helpers.pcaHelper(arrayOfObjects, 'PLCO_GSA', xVal, yVal)
const dropdownLayout = await plco.plot.helpers.pcaCreateDropdownLayout(arrayOfObjects, xVal, yVal)
plco.Plotly.newPlot(div_id, traces, Object.assign({
...layout,
xaxis: { ...layout.xaxis, title: { text: `<b>PC-X ${(xVal || '2')}</b>` } },
yaxis: { ...layout.yaxis, title: { text: `<b>PC-Y ${(yVal || '2')}</b>` } },
}, dropdownLayout), config)
}
xSelector.addEventListener('change', eventListener, false)
ySelector.addEventListener('change', eventListener, false)
const div = document.getElementById(div_id)
div.appendChild(xLabel)
div.appendChild(xSelector)
div.appendChild(yLabel)
div.appendChild(ySelector)
}
if (typeof (define) != 'undefined') {
// define({ proto: plco })
define(['https://cdn.plot.ly/plotly-latest.min.js', 'localforage'], (Plotly, localforage) => {
plco.Plotly = Plotly
plco.localForage = localforage
return plco
})
} else {
// satisfy Plotly dependency already
let s = plco.loadScript('https://cdn.plot.ly/plotly-latest.min.js')
s.then(async s => {
s.onload = () => {
plco.Plotly = Plotly
}
})
let script2 = plco.loadScript('https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js')
script2.then(async s => {
s.onload = () => {
plco.localForage = localforage
}
})
}
plco()