Skip to content

Commit

Permalink
Loading overhaul (#88)
Browse files Browse the repository at this point in the history
* Adjusted positioning and visibility of loading msg

* Minor performance tweaks for rasterized rendering

* Rasterized rendering now shows proper loading messages

* Nothing to see here

* Reverted rendering hook loading stage

* Batch render, but no export

* Dynamic chunking

* Bumped version to 1.11.0

Export adjustments
  • Loading branch information
johnmartins authored Nov 8, 2023
1 parent b989432 commit 7ed4bea
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 109 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "md-analysis-cli",
"version": "1.10.0",
"version": "1.11.0",
"private": true,
"author": {
"name": "Julian Martinsson Bonde",
Expand Down
36 changes: 18 additions & 18 deletions src/components/DataList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

<script setup>
import { reactive, ref, onMounted, onUpdated } from "vue"
import { reactive, ref, onMounted, onUpdated, nextTick } from "vue"
import { storeToRefs } from "pinia"
import {useDataStore} from "@/store/DataStore"
Expand Down Expand Up @@ -86,31 +86,31 @@ function moveCategory (category, n) {
}
function sortBy (category) {
stateStore.setLoading('Sorting data')
async function sortBy (category) {
await stateStore.setLoading('Sorting data')
if (sortCategoryID.value) {
if (sortCategoryID.value === category.id) {
sortReversed.value = !sortReversed.value
}
}
setTimeout(() => {
sortFunction.value = (a,b) => {
let valA = a[category.title]
let valB = b[category.title]
if (!category.usesCategoricalData) {
valA = parseFloat(valA)
valB = parseFloat(valB)
}
if (valA > valB) return sortReversed.value ? -1 : 1
if (valA < valB) return sortReversed.value ? 1 : -1
return 0
await nextTick();
sortFunction.value = (a,b) => {
let valA = a[category.title]
let valB = b[category.title]
if (!category.usesCategoricalData) {
valA = parseFloat(valA)
valB = parseFloat(valB)
}
sortCategoryID.value = category.id
stateStore.clearLoading()
}, 100)
if (valA > valB) return sortReversed.value ? -1 : 1
if (valA < valB) return sortReversed.value ? 1 : -1
return 0
}
sortCategoryID.value = category.id
await stateStore.clearLoading()
}
function toggleDisableEnable (category) {
Expand Down
20 changes: 13 additions & 7 deletions src/components/LoadingModal.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<template>
<div v-if="loadingReason" class="modal-mask loading modal-container">
<div class="message-container anim-pulse">
<span>{{ loadingReason }}</span><br>
<span class="icon"><faicon icon="fa-solid fa-spinner" class="anim-rotate-step-8" /></span>
<span class="icon me-2"><faicon icon="fa-solid fa-spinner" class="anim-rotate-step-8" /></span>
<span>{{ loadingReason }}</span>
</div>
</div>
</template>

<script setup>
import { storeToRefs } from "pinia"
import { storeToRefs } from "pinia";
import {useStateStore} from "@/store/StateStore"
import {useStateStore} from "@/store/StateStore";
const stateStore = useStateStore()
const {loadingReason} = storeToRefs(stateStore)
const stateStore = useStateStore();
const { loadingReason } = storeToRefs(stateStore);
</script>

Expand All @@ -31,6 +31,12 @@ const {loadingReason} = storeToRefs(stateStore)
}
.message-container {
color: black;
color: white;
white-space: nowrap;
height: 40px;
display: flex;
// Align in middle vertically
justify-content: center; /*x-axis*/
align-items: center; /*y-axis*/
}
</style>
36 changes: 27 additions & 9 deletions src/components/plot-layouts/PCPlot/PCPlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div ref="pcpPlot" style="height: 100%; position: relative;" class="svg-container">
<!-- Raster rendering layer -->
<div v-if="PCPStore.renderingType === 'raster'" style="width: 100%; height: 100%;">
<PCPlotPathLayerRaster />
<PCPlotPathLayerRaster ref="rasterLayer" />
</div>
<svg
ref="plotCanvas"
Expand All @@ -26,7 +26,9 @@
<!-- Vector rendering layer -->
<PCPlotPathLayerVector />

<!-- Raster export image -->
<image v-if="PCPStore.renderingType === 'raster' && pathsDataUrl" :href="pathsDataUrl" width="100%" height="100%" :y="getPlotYBounds()[0]" />


<!-- Axis group. Filter for enabled, sort by position, position using index. -->
<g
Expand Down Expand Up @@ -93,7 +95,7 @@
</template>

<script setup>
import { reactive, ref, onMounted, onUpdated, inject, computed, watch} from "vue"
import { reactive, ref, onMounted, onUpdated, inject, computed, watch, nextTick} from "vue"
import { storeToRefs } from "pinia"
import { saveAs } from "file-saver"
Expand Down Expand Up @@ -139,6 +141,7 @@ const dataExcluded = ref([])
// Layout references
const plotCanvas = ref(null)
const pcpPlot = ref(null)
const rasterLayer = ref(null)
const plotParameters = reactive({
padding: 100,
Expand Down Expand Up @@ -336,23 +339,38 @@ function getSelectedCategoryTitle () {
return selectedCategory.value ? selectedCategory.value.title : null
}
function handleExportRequest (format) {
if (activeView.value !== 'pcp') return
async function handleExportRequest (format) {
if (activeView.value !== 'pcp') return;
if (format === 'png') {
exportPNG()
if (PCPStore.renderingType !== 'raster') {
const errorPopup = new Popup('error', 'Invalid export configuration',
'To export to PNG, first change render mode to "Rasterized" under the Plot Options section in the menu.' +
'\n\nAlternatively, you can export to SVG.')
layoutStore.queuePopup(errorPopup)
return
}
try {
await rasterLayer.value.generateDataUrl();
await nextTick();
await exportPNG();
} finally {
PCPStore.pathsDataUrl = null;
}
}
else if (format === 'svg') {
exportSVG()
exportSVG();
}
else {
throw new Error('Unknown format in export request')
throw new Error('Unknown format in export request');
}
}
function exportPNG () {
async function exportPNG () {
const csvElement = plotCanvas.value
saveSvgAsPng(csvElement, 'PCPlot.png', {encoderOptions: 1, backgroundColor: 'white', scale: 2})
await saveSvgAsPng(csvElement, 'PCPlot.png', {encoderOptions: 1, backgroundColor: 'white', scale: 2})
}
function exportSVG () {
Expand Down
131 changes: 79 additions & 52 deletions src/components/plot-layouts/PCPlot/PCPlotPathLayerRaster.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
</template>

<script setup>
import {onMounted, ref, inject, watch} from "vue"
import { storeToRefs } from "pinia"
import * as d3 from "d3"
import { onMounted, ref, inject, watch, nextTick } from "vue";
import { storeToRefs } from "pinia";
import * as d3 from "d3";
// Stores
import {useDataStore} from "../../../store/DataStore"
import {usePCPStore} from "../../../store/PCPStore"
import {useOptionsStore} from "../../../store/OptionsStore"
import {useStateStore} from "../../../store/StateStore"
defineExpose({
generateDataUrl
})
// Store references
const dataStore = useDataStore()
const PCPStore = usePCPStore()
Expand All @@ -37,11 +41,12 @@ eventBus.on('Router.TabChange', (viewName) => {
})
// Canvas draw variables
let pathCanvas = document.createElement('canvas')
let pathCanvas = document.createElement('canvas') // Is not appended to DOM
let ctx = pathCanvas.getContext('2d')
let redrawTimerID = null
onMounted(() => {
canvasContainer.value.appendChild(pathCanvas);
resizeCanvas()
})
Expand All @@ -62,6 +67,14 @@ watch(() => dataStore.enabledCategoriesCount, () => {
restartRedrawCountdown()
})
async function generateDataUrl () {
await stateStore.setLoading('Generating dURL');
const dUrl = pathCanvas.toDataURL();
PCPStore.pathsDataUrl = dUrl;
await stateStore.clearLoading();
}
function restartRedrawCountdown () {
if (PCPStore.renderingType !== 'raster') return
if (stateStore.activeView !== 'pcp') return
Expand All @@ -78,69 +91,83 @@ function restartRedrawCountdown () {
}, refreshDelay)
}
function draw () {
if (PCPStore.renderingType !== 'raster') return
stateStore.loadingReason = 'Redrawing PCP canvas'
const t_draw_start = performance.now()
PCPStore.pathsDataUrl = null
setTimeout(() => {
ctx.clearRect(0, 0, canvasContainer.value.offsetWidth, canvasContainer.value.offsetHeight)
ctx.setTransform(resolution.value,0,0,resolution.value,0,0);
const renderData = (d, color, opacity) => {
ctx.beginPath();
lineGenerator(d)
ctx.lineWidth = 1;
ctx.globalAlpha = opacity;
ctx.strokeStyle = color
ctx.stroke();
ctx.closePath();
}
async function draw () {
if (PCPStore.renderingType !== 'raster') return;
await stateStore.setLoading('Redrawing PCP canvas');
const t_draw_start = performance.now();
PCPStore.pathsDataUrl = null;
// Render excluded data
if (!optionsStore.hideExcluded) {
data.value
.filter(d => !dataStore.dataPointFilterCheck(d))
.forEach(d => renderData(d, '#bfbfbf', optionsStore.excludedDataOpacity))
}
ctx.clearRect(0, 0, canvasContainer.value.offsetWidth, canvasContainer.value.offsetHeight);
ctx.setTransform(resolution.value,0,0,resolution.value,0,0);
let includedDataArray = [];
let excludedDataArray = [];
for (let i = 0; i < data.value.length; i++) {
let d = data.value[i];
// Render included data
data.value
.filter(dataStore.dataPointFilterCheck)
.forEach(d => renderData(d, getLineColor(d), optionsStore.includedDataOpacity))
// Check if included
if (dataStore.dataPointFilterCheck(d)) {
includedDataArray.push(d);
} else {
excludedDataArray.push(d);
}
}
if (!optionsStore.hideExcluded) await batchRender(excludedDataArray, optionsStore.excludedDataOpacity, '#bfbfbf');
await batchRender(includedDataArray, optionsStore.includedDataOpacity);
const t_draw_end = performance.now();
console.debug(`Draw time: ${(t_draw_end - t_draw_start)/1000} [s]`);
await stateStore.clearLoading();
return
}
stateStore.clearLoading()
function renderLine (d, color, opacity) {
ctx.beginPath();
lineGenerator(d);
ctx.lineWidth = 1;
ctx.globalAlpha = opacity;
ctx.strokeStyle = color;
ctx.stroke();
}
const t_draw_end = performance.now()
console.debug(`Draw time: ${(t_draw_end - t_draw_start)/1000} [s]`)
async function batchRender (dataArray, opacity, overrideColor = null) {
let dataArrayLength = dataArray.length;
let chunkSize = dataArrayLength / getChunkCount(dataArrayLength);
for (let i = 0; i < dataArrayLength; i += chunkSize) {
let chunk = dataArray.slice(i, i + chunkSize);
await Promise.all(chunk.map(d => renderLine(d, overrideColor ? overrideColor : getLineColor(d), opacity)));
const dUrl = pathCanvas.toDataURL()
const t_draw_post_url = performance.now()
console.log(`dUrl generated in ${(t_draw_post_url - t_draw_end) / 1000} seconds`)
PCPStore.pathsDataUrl = dUrl
// Pause until next chunk
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}, 50)
function getChunkCount (dataArrayLength) {
let count = parseInt(Math.max(10,Math.min(50, dataArrayLength/250)));
return count;
}
function resizeCanvas () {
async function resizeCanvas () {
if (PCPStore.renderingType !== 'raster') return
if (!canvasContainer.value) return
PCPStore.pathsDataUrl = null // Triggers the image in PCP to become hidden
setTimeout( () => {
const w = canvasContainer.value.offsetWidth
const h = canvasContainer.value.offsetHeight
pathCanvas.width = w * resolution.value
pathCanvas.height = h * resolution.value
pathCanvas.style.width = w + 'px'
pathCanvas.style.height = h + 'px'
restartRedrawCountdown()
}, 250)
await nextTick();
const w = canvasContainer.value.offsetWidth
const h = canvasContainer.value.offsetHeight
pathCanvas.width = w * resolution.value
pathCanvas.height = h * resolution.value
pathCanvas.style.width = w + 'px'
pathCanvas.style.height = h + 'px'
restartRedrawCountdown()
}
function lineGenerator(d) {
let dataCats = Object.keys(d)
let dataCats = Object.keys(d) // TODO: Refactor potential
let dataArray = Array(dataCats.length).fill(null)
for (let i = 0; i < dataCats.length; i++) {
Expand Down
6 changes: 4 additions & 2 deletions src/components/plot-layouts/PCPlot/PCPlotPathLayerVector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import {truncateDecimals} from "@/utils/data-utils"
import {useDataStore} from "../../../store/DataStore"
import {usePCPStore} from "../../../store/PCPStore"
import {useOptionsStore} from "../../../store/OptionsStore"
import { useStateStore } from "@/store/StateStore"
// Store references
const stateStore = useStateStore();
const dataStore = useDataStore()
const PCPStore = usePCPStore()
const optionsStore = useOptionsStore()
Expand All @@ -42,8 +44,8 @@ const {horizontalOffset, axisLength, plotYBounds} = storeToRefs(PCPStore)
const {data} = storeToRefs(dataStore)
function lineGenerator(d) {
let dataCats = Object.keys(d)
let dataArray = Array(dataCats.length).fill(null)
let dataCats = Object.keys(d);
let dataArray = Array(dataCats.length).fill(null);
for (let i = 0; i < dataCats.length; i++) {
let c = dataStore.getCategoryWithName(dataCats[i])
Expand Down
Loading

0 comments on commit 7ed4bea

Please sign in to comment.