Commit df1caf31 authored by Sascha Herzinger's avatar Sascha Herzinger
Browse files

Major changes to integrate canvas for massive performance improvements on large datasets

This is one of many following commits for proper integration
parent a74c02bf
Pipeline #2966 passed with stages
in 3 minutes and 58 seconds
{
"name": "fractalis",
"version": "0.1.8",
"version": "0.1.9",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
......@@ -4,8 +4,8 @@ import RequestManager from './services/request-manager'
import ChartManager from './services/chart-manager'
class FractalJS {
constructor (handler, thisBaseURL, fractalisBaseURL, getAuth) {
const requestManager = new RequestManager({handler, thisBaseURL, fractalisBaseURL, getAuth})
constructor (handler, dataSource, fractalisNode, getAuth) {
const requestManager = new RequestManager({handler, dataSource, fractalisNode, getAuth})
store.dispatch('setRequestManager', requestManager)
store.dispatch('updateData')
this._chartManager = new ChartManager()
......@@ -51,24 +51,24 @@ class FractalJS {
* Initialize FractalJS and return an instance that contains all basic methods necessary to use this library.
*
* @param handler: The service in which this library is used. Example: 'ada', 'tranSMART', 'variantDB'
* @param thisBaseURL: The base URL of the service in which this library is used. Example: 'https://my.service.org/'
* @param fractalisBaseURL: The base URL of the fractalis back end that you want to use. 'http://fractalis.uni.lu/'
* @param dataSource: The base URL of the service in which this library is used. Example: 'https://my.service.org/'
* @param fractalisNode: The base URL of the fractalis back end that you want to use. 'http://fractalis.uni.lu/'
* @param getAuth: This MUST be a function that can be called at any time to retrieve credentials to authenticate with
* the API of the service specified in thisBaseURL.
* the API of the service specified in dataSource.
* @returns {FractalJS}: An instance of FractalJS.
*/
export function init ({handler, thisBaseURL, fractalisBaseURL, getAuth}) {
export function init ({handler, dataSource, fractalisNode, getAuth}) {
if (!handler) {
throw new Error(`handler property must not be ${handler}`)
}
if (!thisBaseURL) {
throw new Error(`handler property must not be ${thisBaseURL}`)
if (!dataSource) {
throw new Error(`handler property must not be ${dataSource}`)
}
if (!fractalisBaseURL) {
throw new Error(`handler property must not be ${fractalisBaseURL}`)
if (!fractalisNode) {
throw new Error(`handler property must not be ${fractalisNode}`)
}
if (!getAuth) {
throw new Error(`handler property must not be ${getAuth}`)
}
return new FractalJS(handler, thisBaseURL, fractalisBaseURL, getAuth)
return new FractalJS(handler, dataSource, fractalisNode, getAuth)
}
......@@ -10,13 +10,13 @@ import store from '../store/store'
* Instead use the provided helpers available as mixins.
*/
export default class {
constructor ({handler, thisBaseURL, fractalisBaseURL, getAuth}) {
constructor ({handler, dataSource, fractalisNode, getAuth}) {
this._handler = handler
this._thisBaseURL = thisBaseURL
this._dataSource = dataSource
this._getAuth = getAuth
this._axios = axios.create({
baseURL: fractalisBaseURL,
baseURL: fractalisNode,
timeout: 30000,
withCredentials: true
})
......@@ -34,7 +34,7 @@ export default class {
descriptors,
auth: this._getAuth(),
handler: this._handler,
server: this._thisBaseURL
server: this._dataSource
})
}
......
......@@ -25,8 +25,7 @@
</div>
</control-panel>
<svg :width="width"
:height="height">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height">
<rect x="0" y="0" :height="height" :width="width" style="opacity: 0;" @click="resetFilter"></rect>
<g :transform="`translate(${margin.left}, ${margin.top})`">
<g class="fjs-boxplot-axis fjs-x-axis" :transform="`translate(0, ${padded.height})`"></g>
......@@ -98,15 +97,7 @@
:width="boxplotWidth"
:height="boxes[label].l_qrt - boxes[label].median">
</rect>
<circle class="fjs-points"
:title="point.tooltip"
v-tooltip="{arrow: true, theme: 'light'}"
:cx="point.jitter"
:cy="scales.y(point.value)"
r="0.4%"
v-for="point in points[label]"
v-if="params.showData">
</circle>
<svg-canvas :z-index="1" :height="padded.height" :width="boxplotWidth / 2"></svg-canvas>
<polyline class="fjs-kde"
:points="kdePolyPoints[label]"
v-if="params.showKDE">
......@@ -128,6 +119,7 @@
import deepFreeze from 'deep-freeze-strict'
import { truncateTextUntil } from '../mixins/utils'
import tooltip from '../directives/tooltip'
import SvgCanvas from '../components/SVGCanvas.vue'
export default {
name: 'boxplot',
data () {
......@@ -163,6 +155,9 @@
subsets: store.getters.subsets
}
},
pointSize () {
return this.width / 150
},
validArgs () {
return this.numData.length > 0
},
......@@ -195,18 +190,9 @@
return {
id: d.id,
value: d.value,
jitter: this.params.jitter ? Math.random() * this.boxplotWidth / 2 : this.boxplotWidth / 2,
jitter: Math.max(this.pointSize / 2, (this.params.jitter ? Math.random() * this.boxplotWidth / 2 : this.boxplotWidth / 2) - this.pointSize / 2),
subset: d.subset,
category: d.category,
get tooltip () {
return `
<div>
<p>${d.feature}: ${this.value}</p>
<p>Category: ${this.category}</p>
<p>Subset: ${this.subset + 1}</p>
</div>
`
}
category: d.category
}
})
})
......@@ -306,10 +292,21 @@
.call(newAxis.y)
})
}
},
'params.showData': {
handler: function () {
this.drawPoints()
}
},
'params.jitter': {
handler: function () {
this.drawPoints()
}
}
},
methods: {
getTippyInstances (label) {
// prepare mouseover event to populare tippy instances (s. tooltip.js)
const event = document.createEvent('Event')
event.initEvent('mouseover', true, true)
return [
......@@ -319,16 +316,15 @@
this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-lower-quartile`),
this.$el.querySelector(`.fjs-box[data-label="${label}"] .fjs-median`)
].map(el => {
el.dispatchEvent(event) // populate tooltips
const uuid = el.getAttribute('data-uuid')
return { el, tip: this._tippyInstances[uuid] }
el.dispatchEvent(event)
return el._tippy
})
},
showTooltip (label) {
this.getTippyInstances(label).forEach(d => d.el._tippy.show())
this.getTippyInstances(label).forEach(d => d.show())
},
hideTooltip (label) {
this.getTippyInstances(label).forEach(d => d.el._tippy.hide())
this.getTippyInstances(label).forEach(d => d.hide())
},
update_numData (ids) {
this.numData = ids
......@@ -344,6 +340,25 @@
store.dispatch('setFilter', {filter: 'ids', value: []})
this.hasSetFilter = true
},
drawPoints () {
Object.keys(this.points).forEach(label => {
const canvas = this.$el.querySelector(`.fjs-box[data-label="${label}"] canvas`)
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (this.params.showData) {
this.points[label].forEach(point => {
ctx.beginPath()
ctx.fillStyle = 'black'
ctx.fillRect(
point.jitter - this.pointSize / 2,
this.scales.y(point.value) - this.pointSize / 2,
this.pointSize,
this.pointSize
)
})
}
})
},
resize ({height, width}) {
this.height = height
this.width = width
......@@ -360,6 +375,7 @@
}
},
components: {
SvgCanvas,
ControlPanel,
DataBox,
Chart
......@@ -389,11 +405,6 @@
stroke: none
fill: rgb(180, 221, 253)
shape-rendering: crispEdges
.fjs-points
stroke: white
stroke-width: 1px
.fjs-points:hover
opacity: 0.4
.fjs-kde
fill: none
stroke: black
......
......@@ -26,8 +26,9 @@
</fieldset>
</control-panel>
<svg :height="height" :width="width">
<svg xmlns="http://www.w3.org/2000/svg" :height="height" :width="width">
<g :transform="`translate(${margin.left}, ${margin.top})`">
<svg-canvas class="fjs-canvas" :width="padded.width" :height="padded.height"></svg-canvas>
<g class="fjs-corr-axis fjs-y-axis-2" :transform="`translate(${padded.width}, 0)`"></g>
<g class="fjs-corr-axis fjs-x-axis-2"></g>
<g class="fjs-corr-axis fjs-x-axis-1" :transform="`translate(0, ${padded.height})`"></g>
......@@ -44,13 +45,6 @@
:transform="`translate(${padded.width + margin.right / 2},${padded.height / 2})rotate(90)`">
{{ shownResults.y_label }}
</text>
<polygon class="fjs-scatterplot-point"
:points="point.shape"
:fill="categoryColors[categories.indexOf(point.category) % categoryColors.length]"
:title="point.tooltip"
v-tooltip
v-for="point in points">
</polygon>
<line class="fjs-lin-reg-line"
:x1="regLine.x1"
:x2="regLine.x2"
......@@ -89,6 +83,7 @@
import * as d3 from 'd3'
import tooltip from '../directives/tooltip.js'
import deepFreeze from 'deep-freeze-strict'
import SvgCanvas from '../components/SVGCanvas.vue'
export default {
name: 'correlation-analysis',
data () {
......@@ -344,9 +339,15 @@
d3.select(this.$el.querySelector('.fjs-brush')).call(newBrush)
})
}
},
'points': {
handler: function (newPoints) {
this.$nextTick(() => this.drawPoints(newPoints))
}
}
},
components: {
SvgCanvas,
ControlPanel,
DataBox,
Chart
......@@ -371,6 +372,21 @@
})
.catch(error => console.error(error))
},
drawPoints (points) {
const canvas = this.$el.querySelector('.fjs-canvas canvas')
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
points.forEach(d => {
ctx.beginPath()
ctx.fillStyle = this.categoryColors[this.categories.indexOf(d.category) % this.categoryColors.length]
ctx.moveTo(d.shape[0], d.shape[1])
for (let i = 2; i < d.shape.length - 1; i += 2) {
ctx.lineTo(d.shape[i], d.shape[i + 1])
}
ctx.closePath()
ctx.fill()
})
},
resize ({height, width}) {
this.height = height
this.width = width
......
......@@ -129,13 +129,8 @@
<svg xmlns="http://www.w3.org/2000/svg" :height="height" :width="width">
<foreignObject :x="margin.left" :y="margin.top"
:width="this.padded.width" :height="this.padded.height">
<body xmlns="http://www.w3.org/1999/xhtml" style="margin: 0">
<canvas :width="this.padded.width" :height="this.padded.height"></canvas>
</body>
</foreignObject>
<g :transform="`translate(${margin.left}, ${margin.top})`">
<svg-canvas class="fjs-canvas" :width="padded.width" :height="padded.height"></svg-canvas>
<rect class="fjs-sig-bar"
:x="bar.x"
:y="bar.y"
......@@ -160,6 +155,7 @@
import * as d3 from 'd3'
import tooltip from '../directives/tooltip.js'
import deepFreeze from 'deep-freeze-strict'
import SvgCanvas from '../components/SVGCanvas.vue'
export default {
name: 'heatmap',
data () {
......@@ -426,7 +422,7 @@
this.numericArrayDataIds = ids
},
drawCells (cells) {
const canvas = this.$el.querySelector('canvas')
const canvas = this.$el.querySelector('.fjs-canvas canvas')
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
cells.forEach(d => {
......@@ -459,6 +455,7 @@
}
},
components: {
SvgCanvas,
ControlPanel,
DataBox,
Chart
......
......@@ -29,9 +29,9 @@
</div>
</control-panel>
<svg :width="width"
:height="height">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height">
<g :transform="`translate(${margin.left}, ${margin.top})`">
<svg-canvas class="fjs-canvas" :width="padded.width" :height="padded.height"></svg-canvas>
<g class="fjs-brush"></g>
<g class="fjs-axis fjs-y-axis-2" :transform="`translate(${padded.width}, 0)`"></g>
<g class="fjs-axis fjs-x-axis-2"></g>
......@@ -48,13 +48,6 @@
v-show="results.data.id.length">
Principal Component {{pcY}} (Variance Ratio: {{ results.variance_ratios[pcY].toFixed(2) }})
</text>
<polygon class="fjs-scatterplot-point"
:points="point.shape"
:fill="categoryColors[categories.indexOf(point.category) % categoryColors.length]"
:title="point.tooltip"
v-tooltip
v-for="point in points">
</polygon>
<g v-for="loading in loadings">
<line class="fjs-loadings"
:x1="loading.x1"
......@@ -71,21 +64,14 @@
<g class="fjs-pc-distribution fjs-pc-x-distribution"
:transform="`translate(0, ${padded.height + margin.bottom / 2})`">
<line :x2="padded.width"></line>
<circle :cx="point.x"
:r="width / 150"
v-for="point in points">
</circle>
<svg-canvas class="fjs-pc-x-dist-canvas" :width="padded.width" :height="width / 150"></svg-canvas>
</g>
<g class="fjs-pc-distribution fjs-pc-y-distribution"
:transform="`translate(${- margin.left / 2}, 0)`">
<line :y2="padded.height"></line>
<circle :cy="point.y"
:r="width / 150"
v-for="point in points">
</circle>
<svg-canvas class="fjs-pc-y-dist-canvas" :width="width / 150" :height="padded.height"></svg-canvas>
</g>
</g>
</g>
</svg>
</chart>
......@@ -101,6 +87,7 @@
import * as d3 from 'd3'
import tooltip from '../directives/tooltip.js'
import deepFreeze from 'deep-freeze-strict'
import SvgCanvas from '../components/SVGCanvas.vue'
export default {
name: 'pca-analysis',
data () {
......@@ -288,6 +275,12 @@
d3.select(this.$el.querySelector('.fjs-brush')).call(newBrush)
})
}
},
'points': {
handler: function (newPoints) {
this.$nextTick(() => this.drawScatterPoints(newPoints))
this.$nextTick(() => this.drawDistPoints(newPoints))
}
}
},
methods: {
......@@ -300,6 +293,34 @@
})
.catch(error => console.error(error))
},
drawScatterPoints (points) {
const canvas = this.$el.querySelector('.fjs-canvas canvas')
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
points.forEach(d => {
ctx.beginPath()
ctx.fillStyle = this.categoryColors[this.categories.indexOf(d.category) % this.categoryColors.length]
ctx.moveTo(d.shape[0], d.shape[1])
for (let i = 2; i < d.shape.length - 1; i += 2) {
ctx.lineTo(d.shape[i], d.shape[i + 1])
}
ctx.closePath()
ctx.fill()
})
},
drawDistPoints (points) {
const xCanvas = this.$el.querySelector('.fjs-pc-x-dist-canvas canvas')
const yCanvas = this.$el.querySelector('.fjs-pc-x-dist-canvas canvas')
const xctx = xCanvas.getContext('2d')
const yctx = yCanvas.getContext('2d')
const rectSize = this.width / 150
points.forEach(d => {
xctx.beginPath()
yctx.beginPath()
xctx.fillRect(d.x - rectSize / 2, rectSize / 2, rectSize, rectSize)
yctx.fillRect(rectSize / 2, d.y - rectSize / 2, rectSize, rectSize)
})
},
resize ({height, width}) {
this.height = height
this.width = width
......@@ -312,6 +333,7 @@
}
},
components: {
SvgCanvas,
ControlPanel,
DataBox,
Chart
......
<template>
<foreignObject :x="x" :y="y" :width="width" :height="height">
<body xmlns="http://www.w3.org/1999/xhtml"
style="margin: 0; position: relative;"
:style="{'z-index': zIndex}">
<canvas :width="width" :height="height"></canvas>
</body>
</foreignObject>
</template>
<script>
export default {
name: 'svg-canvas',
data () {
return {
x: 0,
y: 0
}
},
props: {
height: {
type: Number,
required: true
},
width: {
type: Number,
required: true
},
zIndex: {
type: Number,
default: -1,
required: false
}
},
computed: {
size () {
return [this.width, this.height].join()
}
},
mounted () {
this.computeOffset()
},
watch: {
'size': {
handler: function () {
this.$nextTick(() => {
this.computeOffset()
})
}
}
},
methods: {
// this entire method is only here because browsers are buggy and do not render foreignObject correctly
computeOffset () {
const isFirefox = typeof InstallTrigger !== 'undefined' // detect browser via feature detection
if (isFirefox) {
// Firefox does not need the code below because it works correct
return
}
let xOffset = 0
let yOffset = 0
let currentNode = this.$el.parentElement
while (currentNode.tagName !== 'svg') {
if (currentNode.hasAttribute('transform')) {
const attr = currentNode.getAttribute('transform')
if (attr) {
xOffset += parseFloat(attr.match(/\((.+),/)[1].trim())
yOffset += parseFloat(attr.match(/,(.+)\)/)[1].trim())
}
}
currentNode = currentNode.parentElement
}
this.x = xOffset
this.y = yOffset
}
}
}
</script>
<style scoped>
</style>
import tippy from 'tippy.js'
import uuid4 from 'uuid/v4'
export default {
bind (el, binding, vnode) {
if (typeof vnode.context._tippyInstances === 'undefined') {
vnode.context._tippyInstances = {}
}
bind (el, binding) {
const addTooltip = (event) => {
const uuid = uuid4()
el.setAttribute('data-uuid', uuid)
const target = event.target || event.srcElement
vnode.context._tippyInstances[uuid] = tippy(target, Object.assign({
tippy(target, Object.assign({
performance: true,
arrow: true,
dynamicTitle: true
......
......@@ -37,10 +37,10 @@ export function truncateTextUntil ({text, font, maxWidth}) {
}
export function getPolygonPointsForSubset ({cx, cy, size, subset}) {
const diamond = (cx, cy, size) => `${cx},${cy - size * 0.66} ${cx + size * 0.66},${cy} ${cx},${cy + size * 0.66} ${cx - size * 0.66},${cy}`
const square = (cx, cy, size) => `${cx - size / 2},${cy - size / 2} ${cx + size / 2},${cy - size / 2} ${cx + size / 2},${cy + size / 2} ${cx - size / 2},${cy + size / 2}`
const triangle = (cx, cy, size) => `${cx},${cy - size * 0.66} ${cx + size * 0.66},${cy + size * 0.66} ${cx + size * 0.33},${cy + size * 0.66} ${cx - size * 0.66},${cy + size * 0.66}`
const revTriangle = (cx, cy, size) => `${cx - size * 0.66},${cy - size * 0.66} ${cx - size * 0.33},${cy - size * 0.66} ${cx + size * 0.66},${cy - size * 0.66} ${cx},${cy + size * 0.66}`
const diamond = (cx, cy, size) => [cx, cy - size * 0.66, cx + size * 0.66, cy, cx, cy + size * 0.66, cx - size * 0.66, cy]
const square = (cx, cy, size) => [cx - size / 2, cy - size / 2, cx + size / 2, cy - size / 2, cx + size / 2, cy + size / 2, cx - size / 2, cy + size / 2]
const triangle = (cx, cy, size) => [cx, cy - size * 0.66, cx + size * 0.66, cy + size * 0.66, cx + size * 0.33, cy + size * 0.66, cx - size * 0.66, cy + size * 0.66]
const revTriangle = (cx, cy, size) => [cx - size * 0.66, cy - size * 0.66, cx - size * 0.33, cy - size * 0.66, cx + size * 0.66, cy - size * 0.66, cx, cy + size * 0.66]
const shapes = [diamond, square, revTriangle, triangle]
return shapes[subset % shapes.length](cx, cy, size)
}
<!doctype html>
<head>
<script src="http://localhost:8080/credentials.js"></script>
<script src="http://localhost:8080/fractal.js"></script>
</head>
<body>
<input type="button" onclick="loadData()" value="load data"/>
<input type="button" onclick="deleteData()" value="delete data"/>
<div style="height: 50%; width: 50%">
<div id="placeholder1"></div>
</div>
</body>
<script>
/* eslint-disable */
const fjs = fractal.init({
handler: 'ada',
thisBaseURL: 'https://ada.parkinson.lu',
fractalisBaseURL: 'http://127.0.0.1:5000',
getAuth () {
return credentials1
}
})
function loadData () {
fjs.loadData([
{
dictionary: {
name: '