From 9b5249c30a833287685876d10364ed14e9ba88f3 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 17 Nov 2020 16:33:21 +0100 Subject: [PATCH] fix weave imports frontend and backend --- common/weave.js | 2 +- deploy/common/.gitignore | 1 + deploy/common/weave.esm.js | 542 +++++++++++++++++ deploy/common/weave.js | 548 ------------------ deploy/package.json | 9 +- package.json | 5 +- .../regional-feature-view/readme.md | 29 + .../regional-feature-view.tsx | 5 +- 8 files changed, 583 insertions(+), 558 deletions(-) create mode 100644 deploy/common/.gitignore create mode 100644 deploy/common/weave.esm.js delete mode 100644 deploy/common/weave.js create mode 100644 src/components/regional-feature-view/readme.md diff --git a/common/weave.js b/common/weave.js index b71f1fe..972620e 120000 --- a/common/weave.js +++ b/common/weave.js @@ -1 +1 @@ -../deploy/common/weave.js \ No newline at end of file +../deploy/common/weave.esm.js \ No newline at end of file diff --git a/deploy/common/.gitignore b/deploy/common/.gitignore new file mode 100644 index 0000000..45e4ade --- /dev/null +++ b/deploy/common/.gitignore @@ -0,0 +1 @@ +weave.js diff --git a/deploy/common/weave.esm.js b/deploy/common/weave.esm.js new file mode 100644 index 0000000..1af49da --- /dev/null +++ b/deploy/common/weave.esm.js @@ -0,0 +1,542 @@ +// const d3 = require('d3') +// d3v ^ 5.16.0 + +let d3, mathjax, JSDOM + +// conversion from ex to pixel +// TODO remove fudge factor in future +const fudgeFactor = 6.5 + +const Image = (typeof window !== 'undefined' && window.Image) || (() => { + try { + return require('canvas').Image + } catch (e) { + return null + } +})() + +const btoa = (typeof window !== 'undefined' && window.btoa) || (input => Buffer.from(input).toString('base64')) +class BaseSvg{ + + convertSvgToPng(svgString){ + if (!Image) return Promise.reject(`Image not defined. If in node, did you include canvas as dependency?`) + return new Promise((rs, rj) => { + const canvas = this.document.createElement('canvas') + canvas.width = this.width + canvas.height = this.height + const ctx = canvas.getContext('2d') + ctx.fillStyle = `white` + ctx.fillRect(0, 0, this.width, this.height) + + const img = new Image() + img.onload = () => { + ctx.drawImage(img, 0, 0) + const dataurl = canvas.toDataURL() + rs(dataurl) + } + img.onerror = rj + + img.src = `data:image/svg+xml;base64,${btoa(svgString)}` + }) + } + + static CalculateWidthHeight(svgString, document) { + if (!svgString) throw new Error(`svg needs to be defined`) + + const container = document.createElement('div') + const el = document.createElement('svg') + container.appendChild(el) + document.body.appendChild(container) + el.outerHTML = svgString + const { width, height} = container.children[0].getBoundingClientRect() + document.body.removeChild(container) + return { width, height } + } + + static GenerateLabel(label) { + const { latex } = label + + const trueLatex = latex.replace(/^\$*/, '').replace(/\$*$/, '') //.replace(/^\\+/, '\\') + const _ = mathjax.tex2svg(trueLatex, { display: true }) + const { width, height } = _.children[0].attributes + return { + label: mathjax.startup.adaptor.innerHTML(_), + width, + height + } + } + + constructor(config = {}){ + + const { width, height, margin, fontFamily, cssSetColor } = config + this.width = width || 600 + this.height = height || 600 + this.margin = { + left: 70, + right: 70, + top: 70, + bottom: 70, + ...margin + } + + this.cssSetColor = cssSetColor + + this.fontFamily = fontFamily || `Arial, Helvetica, san-serif` + + const w = (typeof window !== 'undefined' && window) || (() => { + return new JSDOM(``, { runScripts: 'outside-only' }).window + })() + + const { document } = w + this.document = document + this.window = w + + this.redraw() + } + + redraw(){ + this.clientWidth = this.width - this.margin.left - this.margin.right + this.clientHeight = this.height - this.margin.top - this.margin.bottom + } + + appendLabel(svg, label, anchor = {}){ + const { latex, text } = label + const { top, bottom, left, right } = anchor + + // master container of the label + // apply label at the very end + // because element.getBoundingClientRect() method calculates the post transformed + const labelGroup = svg.append('g') + .attr('font-family', this.fontFamily) + .attr(`class`, `label-container`) + + if (latex && mathjax) { + + // MathJax renders the svg inside mjx-container element, which does not seem to redner + // luckily, we can just get the first child, which is the svg element + const _ = mathjax.tex2svg(latex) + const labelSvg = _.children[0] + + // latex container. Transform this to center the svg + // need to append to dom, then getBoundingRect will return non zero values + // NB: do not call too often. tanks performance + const latexContainer = labelGroup.append('g') + latexContainer.node().append(labelSvg) + const { width, height } = labelSvg.getBoundingClientRect() + + // transform to center the svg + const xTranslate = left + ? 0 + : right + ? width * -1 + : width / -2 + + const yTranslate = top + ? 0 + : bottom + ? height * -1 + : height / -2 + latexContainer.attr('transform', `translate(${xTranslate}, ${yTranslate})`) + } else if (text) { + labelGroup.append('text') + .style('text-anchor', left ? 'start' : right ? 'end' : 'middle') + .attr('dominant-baseline', top ? 'hanging' : bottom ? 'baseline' : 'middle') + .text(text) + } + + return labelGroup + } + + generateSvg(){ + throw new Error(`Needs to be overwritten by subclasses`) + } + + generateD3(){ + throw new Error(`Needs to be overwritten by subclasses`) + } +} + +class PolarSvg extends BaseSvg{ + constructor(data, config){ + super(config) + + this.polarData = data.map(({ density, ...rest }) => { + return { + ...rest, + density: { + ...density, + mean: Number(density.mean), + sd: Number(density.sd) + } + } + }) + + this.diameter = Math.min(this.clientWidth, this.clientHeight) + + const { + + } = config || {} + + this.meanAttrs = this.cssSetColor + ? {} + : { fill: () => `rgba(200, 200, 200, 1.0)` } + + this.sdAttrs = this.cssSetColor + ? {} + : { + fill: () => `none`, + 'stroke-width': () => '1px' , + stroke: () => 'rgba(100, 100, 100, 1.0)' + } + + } + + setMetadata(metadata) { + this.metadata = metadata + } + + generateSvg(){ + const svgMain = this.generateD3(this.document.body) + svgMain.attr('xmlns', 'http://www.w3.org/2000/svg') + const svgToReturn = svgMain.node().outerHTML + this.document.body.removeChild(svgMain.node()) + return svgToReturn + } + + generateD3(hostElement){ + if (!hostElement) throw new Error(`hostElement is undefined!`) + const svgMain = d3.select(hostElement) + .append('svg') + .attr('xmlns', 'http://www.w3.org/2000/svg') + .attr('width', this.width) + .attr('height', this.height) + const svg = svgMain + .append('g') + .attr('transform', `translate(${this.margin.left}, ${this.margin.top})`) + + // define linear scale + this.linearScale = d3 + .scaleLinear() + .range([ 0, this.diameter / 2 ]) + .domain([ 0 , d3.max(this.polarData.map(({ density }) => density.mean + density.sd)) ]) + + + // define mean polar + this.meanRadial = d3.lineRadial() + .angle((d, i) => Math.PI * 2 / (this.polarData.length) * i) + .radius(({ density }) => this.linearScale(density.mean)) + + // define sd polar + this.sdRadial = d3.lineRadial() + .angle((d, index) => Math.PI * 2 / (this.polarData.length) * index) + .radius(({ density }) => this.linearScale(density.mean + density.sd)) + .curve(d3.curveLinearClosed) + + // center of polar graph + const graphContainer = svg.append('g') + .attr('transform', `translate(${this.diameter / 2}, ${this.diameter / 2})`) + + // render mean + const mean = graphContainer.append('path') + .attr('class', 'fill-path') + .data( [ this.polarData ] ) + .attr('d', this.meanRadial) + + for (const key in this.meanAttrs) { + mean.attr(key, this.meanAttrs[key]) + } + + // render sd + const sd = graphContainer.append('path') + .attr('class', 'line-path sd-line-path') + .data( [this.polarData] ) + .attr('d', this.sdRadial) + + for (const key in this.sdAttrs) { + sd.attr(key, this.sdAttrs[key]) + } + + // render marking cricle + for (const rTick of this.linearScale.ticks(5).slice(1)) { + graphContainer.append('circle') + .attr('r', this.linearScale(rTick)) + .attr('stroke-width', '1px') + .attr('stroke-dasharray', '1,4') + .attr('fill', 'none') + .attr('stroke', 'rgba(150, 150, 150, 1.0)') + + // render tick markings for marking circle + graphContainer.append('text') + .attr('font-family', this.fontFamily) + .attr('fill', 'rgba(100, 100, 100, 1.0)') + .attr('dominant-baseline', 'middle') + .style('text-anchor', 'end') + .attr('font-size', 'smaller') + .attr('transform', `translate(-10, -${this.linearScale(rTick)})`) + .text(rTick) + } + + // append radial guide lines + const receptors = this.polarData.map(({ receptor }) => { + const { label: cLabel } = receptor + const found = this.metadata.find(({ receptor: mReceptor }) => mReceptor.label === cLabel) + return found && found.receptor || null + }) + + const radialGuideLineContainers = graphContainer.selectAll('g') + .data( receptors.map(PolarSvg.GenerateLabel) ) + .enter() + .append('g') + .attr('class', (_, idx) => `radialGuideLine radialGuideLine-${idx}`) + .attr('transform', (_receptor, index, array) => `rotate(${ 360 / array.length * index - 90})`) + + const radialGuideLines = radialGuideLineContainers + .append('line') + .attr('class', 'radial-guidelines') + .attr('stroke-width', '1px') + .attr('stroke-dasharray', '8,2') + .attr('x2', this.diameter / 2) + + if (!this.cssSetColor) { + radialGuideLines.attr('stroke', 'rgba(200, 200, 200, 1.0)') + } + + radialGuideLineContainers + .append('g') + .attr('transform', (svg, index, array) => { + const { label } = svg + + const translateX = this.diameter / 2 + return `translate(${translateX}, 0)` + }) + .append('g') + .attr('width', (svg, index, array) => { + return 0 + }) + .attr(`transform`, (svg, index, array) => { + const width = typeof svg.width === 'string' || typeof svg.width === 'number' + ? svg.width + : svg.width.value + + const height = typeof svg.height === 'string' || typeof svg.height === 'number' + ? svg.height + : svg.height.value + const rot = 360 / array.length * index - 90 + const flip = rot > 90 && rot < 270 + const w = Number(width.replace('ex', '')) * fudgeFactor + const h = Number(height.replace('ex', '')) * fudgeFactor / 2 + const translateTxt = flip ? `translate(-${w}, -${h})` : `translate(0, -${h})` + return `rotate(${-1 * rot}) ${translateTxt}` + }) + .html(({label}) => label) + + + // append legends + const lCongainer = svg.append('g') + .attr('fill', 'currentColor') + .attr('transform', `translate(0, -40)`) + + + const keys = Object.keys(this.polarData[0].density) + const meanLabel = keys[0] + const sdLabel = keys[1] + const unit = this.polarData[0].density[keys[2]] + + // mean legend + const lc1 = lCongainer.append('g') + const meanRect = lc1.append('rect') + .attr('class', 'fill-path') + .attr('width', 38) + .attr('height', 16) + + for (const key in this.meanAttrs) { + meanRect.attr(key, this.meanAttrs[key]) + } + + lc1.append('text') + .attr('font-family', this.fontFamily) + .attr('x', 45) + .attr('y', 10) + .text(`${meanLabel} (${unit})`) + + + // sd legend + const lc2 = lCongainer.append('g') + .attr('transform', `translate(0, 20)`) + + const sdRect = lc2.append('rect') + .attr('class', 'line-path sd-line-path') + .attr('width', 38) + .attr('height', 16) + + for (const key in this.sdAttrs) { + sdRect.attr(key, this.sdAttrs[key]) + } + + lc2.append('text') + .attr('font-family', this.fontFamily) + .attr('x', 45) + .attr('y', 10) + .text(`${sdLabel} (${unit})`) + + return svgMain + } +} + +class LinearSvg extends BaseSvg{ + constructor(data, config){ + super(config) + this.linearData = data.map(({ density }, idx) => [idx, Number(density)]) + const { xAxis, yAxis } = config || {} + this.xAxis = xAxis + this.yAxis = yAxis + } + generateD3(hostElement){ + if (!hostElement) throw new Error(`hostElement is undefined!`) + this.scaleX = d3.scaleLinear().range([0, this.clientWidth]) + this.scaleY = d3.scaleLinear().range([this.clientHeight, 0]) + const svgMain = d3.select(hostElement) + .append('svg') + .attr('width', this.width) + .attr('height', this.height) + const svg = svgMain + .append('g') + .attr('transform', `translate(${this.margin.left}, ${this.margin.top})`) + + const extents = d3.transpose( + d3.extent(this.linearData) + ) + + this.scaleX.domain( [0 , extents[0][1] + 5] ) + this.scaleY.domain([ 0, d3.max( this.linearData.map(v => v[1]) ) ]) + + const area = d3.area() + .x(d => this.scaleX(d[0])) + .y0(this.clientHeight) + .y1(d => this.scaleY(d[1])) + + const line = d3.line() + .x(d => this.scaleX(d[0])) + .y(d => this.scaleY(d[1])) + + const fillPath = svg.append('g') + .append('path') + .attr('class', 'fill-path') + .data( [this.linearData] ) + .attr('d', area) + + if (!this.cssSetColor) { + fillPath.attr('fill', `rgba(200, 200, 200, 1.0)`) + } + + const linePath = svg.append('path') + .attr('class', 'line-path') + .attr('fill', 'none') + .attr('stroke-width', '1px') + .data( [this.linearData] ) + .attr('d', line) + + if (!this.cssSetColor) { + linePath.attr('stroke', `rgba(100,100,100,1.0)`) + } + + svg.append('g') + .attr('transform', `translate(0, ${this.clientHeight})`) + .attr(`class`, `axis`) + .call(d3.axisBottom(this.scaleX)) + + svg.append('g') + .attr(`class`, `axis`) + .call(d3.axisLeft(this.scaleY)) + + // append xaxis label + + if (this.xAxis) { + const { label } = this.xAxis || {} + const labelGroup = this.appendLabel(svg, label) + labelGroup.attr('transform', `translate(${this.clientWidth / 2}, ${this.clientHeight + 35})`) + } + + // append yaxis label + + if(this.yAxis) { + const { label } = this.yAxis || {} + const labelGroup = this.appendLabel(svg, label) + labelGroup.attr('transform', `translate(-35, ${this.clientHeight / 2 }) rotate(-90)`) + } + + return svgMain + } + + generateSvg(){ + const svgMain = this.generateD3(this.document.body) + svgMain.attr('xmlns', 'http://www.w3.org/2000/svg') + const svgToReturn = svgMain.node().outerHTML + this.document.body.removeChild(svgMain.node()) + return svgToReturn + } +} + +const parseReceptorMetadata = tsv => d3.tsvParse(tsv, data => { + return { + receptor: { + label: data['receptor (label)'], + latex: data['receptor (label_latex)'], + markdown: data['receptor (label_markdown)'], + fullname: data['receptor (full name)'], + }, + neurotransmitter: { + label: data['neurotransmitter (label)'], + latex: data['neurotransmitter (label_latex)'], + markdown: data['neurotransmitter (label_markdown)'], + fullname: data['neurotransmitter (full name)'], + } + } +}) + +const parseFingerprint= tsv => d3.tsvParse(tsv, data => { + const key = Object.keys(data).find(key => /receptor label/i.test(key)) + return { + receptor: { + label: data[key] + }, + density: { + mean: data['density (mean)'], + sd: data['density (sd)'], + unit: data['density (unit)'] + } + } +}) + +const parseReceptorProfile = tsv => d3.tsvParse(tsv, data => { + return { + corticalDepth: data['cortical depth (value)'], + corticalDepthUnit: data['cortical depth (unit)'], + density: data['receptor density (value)'], + densityUnit: data[`receptor density (unit)`], + } +}) + +export const weave = (d3Lib, mathjaxLib = null, _JSDOM = null) => { + if (!d3Lib) throw new Error(`need to pass d3 instance`) + if (mathjaxLib){ + if (!mathjaxLib.tex2svg) { + console.warn(`mathjax is provided, but texToSvg method is not defined!`) + } else if (typeof mathjaxLib.tex2svg !== 'function') { + console.warn(`mathjax is provided, texToSvg is defined, but is not a function!`) + } else { + console.log(`math jax provied. will render latex`) + mathjax = mathjaxLib + } + } + d3 = d3Lib + JSDOM = _JSDOM + return { + parseReceptorMetadata, + parseFingerprint, + parseReceptorProfile, + LinearSvg, + PolarSvg, + } +} + diff --git a/deploy/common/weave.js b/deploy/common/weave.js deleted file mode 100644 index 2eefa6d..0000000 --- a/deploy/common/weave.js +++ /dev/null @@ -1,548 +0,0 @@ -// const d3 = require('d3') -// d3v ^ 5.16.0 - -(function(exports){ - let d3, mathjax, JSDOM - - // conversion from ex to pixel - // TODO remove fudge factor in future - const fudgeFactor = 6.5 - - const Image = (typeof window !== 'undefined' && window.Image) || (() => { - try { - return require('canvas').Image - } catch (e) { - return null - } - })() - - const btoa = (typeof window !== 'undefined' && window.btoa) || (input => Buffer.from(input).toString('base64')) - class BaseSvg{ - - convertSvgToPng(svgString){ - if (!Image) return Promise.reject(`Image not defined. If in node, did you include canvas as dependency?`) - return new Promise((rs, rj) => { - const canvas = this.document.createElement('canvas') - canvas.width = this.width - canvas.height = this.height - const ctx = canvas.getContext('2d') - ctx.fillStyle = `white` - ctx.fillRect(0, 0, this.width, this.height) - - const img = new Image() - img.onload = () => { - ctx.drawImage(img, 0, 0) - const dataurl = canvas.toDataURL() - rs(dataurl) - } - img.onerror = rj - - img.src = `data:image/svg+xml;base64,${btoa(svgString)}` - }) - } - - static CalculateWidthHeight(svgString, document) { - if (!svgString) throw new Error(`svg needs to be defined`) - - const container = document.createElement('div') - const el = document.createElement('svg') - container.appendChild(el) - document.body.appendChild(container) - el.outerHTML = svgString - const { width, height} = container.children[0].getBoundingClientRect() - document.body.removeChild(container) - return { width, height } - } - - static GenerateLabel(label) { - const { latex } = label - - const trueLatex = latex.replace(/^\$*/, '').replace(/\$*$/, '') //.replace(/^\\+/, '\\') - const _ = mathjax.tex2svg(trueLatex, { display: true }) - const { width, height } = _.children[0].attributes - return { - label: mathjax.startup.adaptor.innerHTML(_), - width, - height - } - } - - constructor(config = {}){ - - const { width, height, margin, fontFamily, cssSetColor } = config - this.width = width || 600 - this.height = height || 600 - this.margin = { - left: 70, - right: 70, - top: 70, - bottom: 70, - ...margin - } - - this.cssSetColor = cssSetColor - - this.fontFamily = fontFamily || `Arial, Helvetica, san-serif` - - const w = (typeof window !== 'undefined' && window) || (() => { - return new JSDOM(``, { runScripts: 'outside-only' }).window - })() - - const { document } = w - this.document = document - this.window = w - - this.redraw() - } - - redraw(){ - this.clientWidth = this.width - this.margin.left - this.margin.right - this.clientHeight = this.height - this.margin.top - this.margin.bottom - } - - appendLabel(svg, label, anchor = {}){ - const { latex, text } = label - const { top, bottom, left, right } = anchor - - // master container of the label - // apply label at the very end - // because element.getBoundingClientRect() method calculates the post transformed - const labelGroup = svg.append('g') - .attr('font-family', this.fontFamily) - .attr(`class`, `label-container`) - - if (latex && mathjax) { - - // MathJax renders the svg inside mjx-container element, which does not seem to redner - // luckily, we can just get the first child, which is the svg element - const _ = mathjax.tex2svg(latex) - const labelSvg = _.children[0] - - // latex container. Transform this to center the svg - // need to append to dom, then getBoundingRect will return non zero values - // NB: do not call too often. tanks performance - const latexContainer = labelGroup.append('g') - latexContainer.node().append(labelSvg) - const { width, height } = labelSvg.getBoundingClientRect() - - // transform to center the svg - const xTranslate = left - ? 0 - : right - ? width * -1 - : width / -2 - - const yTranslate = top - ? 0 - : bottom - ? height * -1 - : height / -2 - latexContainer.attr('transform', `translate(${xTranslate}, ${yTranslate})`) - } else if (text) { - labelGroup.append('text') - .style('text-anchor', left ? 'start' : right ? 'end' : 'middle') - .attr('dominant-baseline', top ? 'hanging' : bottom ? 'baseline' : 'middle') - .text(text) - } - - return labelGroup - } - - generateSvg(){ - throw new Error(`Needs to be overwritten by subclasses`) - } - - generateD3(){ - throw new Error(`Needs to be overwritten by subclasses`) - } - } - - class PolarSvg extends BaseSvg{ - constructor(data, config){ - super(config) - - this.polarData = data.map(({ density, ...rest }) => { - return { - ...rest, - density: { - ...density, - mean: Number(density.mean), - sd: Number(density.sd) - } - } - }) - - this.diameter = Math.min(this.clientWidth, this.clientHeight) - - const { - - } = config || {} - - this.meanAttrs = this.cssSetColor - ? {} - : { fill: () => `rgba(200, 200, 200, 1.0)` } - - this.sdAttrs = this.cssSetColor - ? {} - : { - fill: () => `none`, - 'stroke-width': () => '1px' , - stroke: () => 'rgba(100, 100, 100, 1.0)' - } - - } - - setMetadata(metadata) { - this.metadata = metadata - } - - generateSvg(){ - const svgMain = this.generateD3(this.document.body) - svgMain.attr('xmlns', 'http://www.w3.org/2000/svg') - const svgToReturn = svgMain.node().outerHTML - this.document.body.removeChild(svgMain.node()) - return svgToReturn - } - - generateD3(hostElement){ - if (!hostElement) throw new Error(`hostElement is undefined!`) - const svgMain = d3.select(hostElement) - .append('svg') - .attr('xmlns', 'http://www.w3.org/2000/svg') - .attr('width', this.width) - .attr('height', this.height) - const svg = svgMain - .append('g') - .attr('transform', `translate(${this.margin.left}, ${this.margin.top})`) - - // define linear scale - this.linearScale = d3 - .scaleLinear() - .range([ 0, this.diameter / 2 ]) - .domain([ 0 , d3.max(this.polarData.map(({ density }) => density.mean + density.sd)) ]) - - - // define mean polar - this.meanRadial = d3.lineRadial() - .angle((d, i) => Math.PI * 2 / (this.polarData.length) * i) - .radius(({ density }) => this.linearScale(density.mean)) - - // define sd polar - this.sdRadial = d3.lineRadial() - .angle((d, index) => Math.PI * 2 / (this.polarData.length) * index) - .radius(({ density }) => this.linearScale(density.mean + density.sd)) - .curve(d3.curveLinearClosed) - - // center of polar graph - const graphContainer = svg.append('g') - .attr('transform', `translate(${this.diameter / 2}, ${this.diameter / 2})`) - - // render mean - const mean = graphContainer.append('path') - .attr('class', 'fill-path') - .data( [ this.polarData ] ) - .attr('d', this.meanRadial) - - for (const key in this.meanAttrs) { - mean.attr(key, this.meanAttrs[key]) - } - - // render sd - const sd = graphContainer.append('path') - .attr('class', 'line-path sd-line-path') - .data( [this.polarData] ) - .attr('d', this.sdRadial) - - for (const key in this.sdAttrs) { - sd.attr(key, this.sdAttrs[key]) - } - - // render marking cricle - for (const rTick of this.linearScale.ticks(5).slice(1)) { - graphContainer.append('circle') - .attr('r', this.linearScale(rTick)) - .attr('stroke-width', '1px') - .attr('stroke-dasharray', '1,4') - .attr('fill', 'none') - .attr('stroke', 'rgba(150, 150, 150, 1.0)') - - // render tick markings for marking circle - graphContainer.append('text') - .attr('font-family', this.fontFamily) - .attr('fill', 'rgba(100, 100, 100, 1.0)') - .attr('dominant-baseline', 'middle') - .style('text-anchor', 'end') - .attr('font-size', 'smaller') - .attr('transform', `translate(-10, -${this.linearScale(rTick)})`) - .text(rTick) - } - - // append radial guide lines - const receptors = this.polarData.map(({ receptor }) => { - const { label: cLabel } = receptor - const found = this.metadata.find(({ receptor: mReceptor }) => mReceptor.label === cLabel) - return found && found.receptor || null - }) - - const radialGuideLineContainers = graphContainer.selectAll('g') - .data( receptors.map(PolarSvg.GenerateLabel) ) - .enter() - .append('g') - .attr('class', (_, idx) => `radialGuideLine radialGuideLine-${idx}`) - .attr('transform', (_receptor, index, array) => `rotate(${ 360 / array.length * index - 90})`) - - const radialGuideLines = radialGuideLineContainers - .append('line') - .attr('class', 'radial-guidelines') - .attr('stroke-width', '1px') - .attr('stroke-dasharray', '8,2') - .attr('x2', this.diameter / 2) - - if (!this.cssSetColor) { - radialGuideLines.attr('stroke', 'rgba(200, 200, 200, 1.0)') - } - - radialGuideLineContainers - .append('g') - .attr('transform', (svg, index, array) => { - const { label } = svg - - const translateX = this.diameter / 2 - return `translate(${translateX}, 0)` - }) - .append('g') - .attr('width', (svg, index, array) => { - return 0 - }) - .attr(`transform`, (svg, index, array) => { - const width = typeof svg.width === 'string' || typeof svg.width === 'number' - ? svg.width - : svg.width.value - - const height = typeof svg.height === 'string' || typeof svg.height === 'number' - ? svg.height - : svg.height.value - const rot = 360 / array.length * index - 90 - const flip = rot > 90 && rot < 270 - const w = Number(width.replace('ex', '')) * fudgeFactor - const h = Number(height.replace('ex', '')) * fudgeFactor / 2 - const translateTxt = flip ? `translate(-${w}, -${h})` : `translate(0, -${h})` - return `rotate(${-1 * rot}) ${translateTxt}` - }) - .html(({label}) => label) - - - // append legends - const lCongainer = svg.append('g') - .attr('fill', 'currentColor') - .attr('transform', `translate(0, -40)`) - - - const keys = Object.keys(this.polarData[0].density) - const meanLabel = keys[0] - const sdLabel = keys[1] - const unit = this.polarData[0].density[keys[2]] - - // mean legend - const lc1 = lCongainer.append('g') - const meanRect = lc1.append('rect') - .attr('class', 'fill-path') - .attr('width', 38) - .attr('height', 16) - - for (const key in this.meanAttrs) { - meanRect.attr(key, this.meanAttrs[key]) - } - - lc1.append('text') - .attr('font-family', this.fontFamily) - .attr('x', 45) - .attr('y', 10) - .text(`${meanLabel} (${unit})`) - - - // sd legend - const lc2 = lCongainer.append('g') - .attr('transform', `translate(0, 20)`) - - const sdRect = lc2.append('rect') - .attr('class', 'line-path sd-line-path') - .attr('width', 38) - .attr('height', 16) - - for (const key in this.sdAttrs) { - sdRect.attr(key, this.sdAttrs[key]) - } - - lc2.append('text') - .attr('font-family', this.fontFamily) - .attr('x', 45) - .attr('y', 10) - .text(`${sdLabel} (${unit})`) - - return svgMain - } - } - - class LinearSvg extends BaseSvg{ - constructor(data, config){ - super(config) - this.linearData = data.map(({ density }, idx) => [idx, Number(density)]) - const { xAxis, yAxis } = config || {} - this.xAxis = xAxis - this.yAxis = yAxis - } - generateD3(hostElement){ - if (!hostElement) throw new Error(`hostElement is undefined!`) - this.scaleX = d3.scaleLinear().range([0, this.clientWidth]) - this.scaleY = d3.scaleLinear().range([this.clientHeight, 0]) - const svgMain = d3.select(hostElement) - .append('svg') - .attr('width', this.width) - .attr('height', this.height) - const svg = svgMain - .append('g') - .attr('transform', `translate(${this.margin.left}, ${this.margin.top})`) - - const extents = d3.transpose( - d3.extent(this.linearData) - ) - - this.scaleX.domain( [0 , extents[0][1] + 5] ) - this.scaleY.domain([ 0, d3.max( this.linearData.map(v => v[1]) ) ]) - - const area = d3.area() - .x(d => this.scaleX(d[0])) - .y0(this.clientHeight) - .y1(d => this.scaleY(d[1])) - - const line = d3.line() - .x(d => this.scaleX(d[0])) - .y(d => this.scaleY(d[1])) - - const fillPath = svg.append('g') - .append('path') - .attr('class', 'fill-path') - .data( [this.linearData] ) - .attr('d', area) - - if (!this.cssSetColor) { - fillPath.attr('fill', `rgba(200, 200, 200, 1.0)`) - } - - const linePath = svg.append('path') - .attr('class', 'line-path') - .attr('fill', 'none') - .attr('stroke-width', '1px') - .data( [this.linearData] ) - .attr('d', line) - - if (!this.cssSetColor) { - linePath.attr('stroke', `rgba(100,100,100,1.0)`) - } - - svg.append('g') - .attr('transform', `translate(0, ${this.clientHeight})`) - .attr(`class`, `axis`) - .call(d3.axisBottom(this.scaleX)) - - svg.append('g') - .attr(`class`, `axis`) - .call(d3.axisLeft(this.scaleY)) - - // append xaxis label - - if (this.xAxis) { - const { label } = this.xAxis || {} - const labelGroup = this.appendLabel(svg, label) - labelGroup.attr('transform', `translate(${this.clientWidth / 2}, ${this.clientHeight + 35})`) - } - - // append yaxis label - - if(this.yAxis) { - const { label } = this.yAxis || {} - const labelGroup = this.appendLabel(svg, label) - labelGroup.attr('transform', `translate(-35, ${this.clientHeight / 2 }) rotate(-90)`) - } - - return svgMain - } - - generateSvg(){ - const svgMain = this.generateD3(this.document.body) - svgMain.attr('xmlns', 'http://www.w3.org/2000/svg') - const svgToReturn = svgMain.node().outerHTML - this.document.body.removeChild(svgMain.node()) - return svgToReturn - } - } - - const parseReceptorMetadata = tsv => d3.tsvParse(tsv, data => { - return { - receptor: { - label: data['receptor (label)'], - latex: data['receptor (label_latex)'], - markdown: data['receptor (label_markdown)'], - fullname: data['receptor (full name)'], - }, - neurotransmitter: { - label: data['neurotransmitter (label)'], - latex: data['neurotransmitter (label_latex)'], - markdown: data['neurotransmitter (label_markdown)'], - fullname: data['neurotransmitter (full name)'], - } - } - }) - - const parseFingerprint= tsv => d3.tsvParse(tsv, data => { - const key = Object.keys(data).find(key => /receptor label/i.test(key)) - return { - receptor: { - label: data[key] - }, - density: { - mean: data['density (mean)'], - sd: data['density (sd)'], - unit: data['density (unit)'] - } - } - }) - - const parseReceptorProfile = tsv => d3.tsvParse(tsv, data => { - return { - corticalDepth: data['cortical depth (value)'], - corticalDepthUnit: data['cortical depth (unit)'], - density: data['receptor density (value)'], - densityUnit: data[`receptor density (unit)`], - } - }) - - exports.weave = (d3Lib, mathjaxLib = null, _JSDOM = null) => { - if (!d3Lib) throw new Error(`need to pass d3 instance`) - if (mathjaxLib){ - if (!mathjaxLib.tex2svg) { - console.warn(`mathjax is provided, but texToSvg method is not defined!`) - } else if (typeof mathjaxLib.tex2svg !== 'function') { - console.warn(`mathjax is provided, texToSvg is defined, but is not a function!`) - } else { - console.log(`math jax provied. will render latex`) - mathjax = mathjaxLib - } - } - d3 = d3Lib - JSDOM = _JSDOM - return { - parseReceptorMetadata, - parseFingerprint, - parseReceptorProfile, - LinearSvg, - PolarSvg, - } - } - -})(typeof exports === 'undefined' - ? typeof module === 'undefined' - ? window - : module.exports - : exports) diff --git a/deploy/package.json b/deploy/package.json index 8eb11fa..f8172f4 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -4,8 +4,10 @@ "description": "", "main": "index.js", "scripts": { + "prestart": "echo transpiling esm to cjs for nodejs && rollup ./common/weave.esm.js --file ./common/weave.js --format cjs", "start": "node server.js", - "mocha": "mocha" + "mocha": "mocha", + "rollup": "rollup" }, "keywords": [], "author": "", @@ -23,10 +25,11 @@ "rate-limit-redis": "^2.0.0", "redis": "^3.0.2", "sharp": "^0.25.4", - "tmp": "^0.2.1" + "tmp": "^0.2.1", + "rollup": "^2.33.2" }, "devDependencies": { "chai": "^4.2.0", - "mocha": "^8.0.1" + "mocha": "^8.2.1" } } diff --git a/package.json b/package.json index 9b9e8ca..666a7bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kg-dataset-previewer", - "version": "1.1.0", + "version": "1.1.1", "description": "Preview a few manually curated datasets.", "main": "dist/index.js", "module": "dist/index.mjs", @@ -12,8 +12,7 @@ "unpkg": "dist/kg-dataset-previewer/kg-dataset-previewer.js", "files": [ "dist/", - "loader/", - "deploy/" + "loader/" ], "scripts": { "build": "stencil build --docs", diff --git a/src/components/regional-feature-view/readme.md b/src/components/regional-feature-view/readme.md new file mode 100644 index 0000000..298138c --- /dev/null +++ b/src/components/regional-feature-view/readme.md @@ -0,0 +1,29 @@ +# kg-ds-prv-regional-feature-view + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------- | ----------------------- | ----------- | --------- | ---------------------------------- | +| `backendUrl` | `kg-ds-prv-backend-url` | | `string` | `KG_DATASET_PREVIEWER_BACKEND_URL` | +| `darkmode` | `kg-ds-prv-darkmode` | | `boolean` | `false` | +| `filename` | `kg-ds-prv-filename` | | `string` | `undefined` | +| `injectedData` | `kg-ds-prv-data` | | `any` | `undefined` | +| `kgId` | `kg-ds-prv-kg-id` | | `string` | `undefined` | +| `kgSchema` | `kg-ds-prv-kg-schema` | | `string` | ``minds/core/dataset/v1.0.0`` | + + +## Events + +| Event | Description | Type | +| -------------------------------------- | ----------- | ------------------ | +| `kg-ds-prv-regional-feature-mouseover` | | `CustomEvent` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/components/regional-feature-view/regional-feature-view.tsx b/src/components/regional-feature-view/regional-feature-view.tsx index e6a93fb..77d0959 100644 --- a/src/components/regional-feature-view/regional-feature-view.tsx +++ b/src/components/regional-feature-view/regional-feature-view.tsx @@ -1,7 +1,6 @@ import { Component, Prop, h, Watch, State, Event, EventEmitter } from "@stencil/core"; import { KG_DATASET_PREVIEWER_BACKEND_URL } from "../../utils/utils"; -import '../../../common/weave' - +import { weave } from '../../../common/weave' let parseReceptorProfile, LinearSvg, parseFingerprint, PolarSvg, parseReceptorMetadata @@ -126,7 +125,7 @@ export class RegionalFeatureView{ parseFingerprint: pRfp, PolarSvg: PSvg, parseReceptorMetadata: pRMeta - } = window['weave'](window['d3'], window['MathJax']) + } = weave(window['d3'], window['MathJax']) parseReceptorProfile = prp LinearSvg = LSvg PolarSvg = PSvg