Skip to content

Commit ad7810f

Browse files
- ADD: Added an overview view of a trial that can be saved as an image and potentially printed.
- ADD: Added vertical stepper buttons to the top of each section to make it more obvious how to get back to the previous step.
1 parent 62e9c12 commit ad7810f

13 files changed

Lines changed: 351 additions & 39 deletions

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@vueuse/components": "^13.9.0",
1919
"@vueuse/core": "^13.9.0",
2020
"axios": "^1.12.2",
21+
"canvas-size": "^2.0.0",
2122
"compressorjs": "^1.2.1",
2223
"d3-dsv": "^3.0.1",
2324
"file-saver": "^2.0.5",
@@ -44,6 +45,7 @@
4445
},
4546
"devDependencies": {
4647
"@tsconfig/node22": "^22.0.0",
48+
"@types/canvas-size": "^1.2.2",
4749
"@types/d3-dsv": "^3.0.7",
4850
"@types/file-saver": "^2.0.7",
4951
"@types/leaflet": "^1.9.20",

src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ declare module 'vue' {
6161
PlotDataInformation: typeof import('./components/plot/PlotDataInformation.vue')['default']
6262
PlotInformation: typeof import('./components/plot/PlotInformation.vue')['default']
6363
PlotTraitCompletionChart: typeof import('./components/chart/PlotTraitCompletionChart.vue')['default']
64+
PrintCanvas: typeof import('./components/data/PrintCanvas.vue')['default']
6465
QRScanInput: typeof import('./components/inputs/QRScanInput.vue')['default']
6566
ReplicateHeatmap: typeof import('./components/chart/ReplicateHeatmap.vue')['default']
6667
ResponsiveButton: typeof import('./components/util/ResponsiveButton.vue')['default']
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<template>
2+
<div class="h-100 position-relative">
3+
<p class="text-error" v-if="errorMessage">{{ $t(errorMessage) }}</p>
4+
<v-overlay contained class="align-center justify-center" v-model="loading">
5+
<v-progress-circular indeterminate color="primary" size="48" />
6+
</v-overlay>
7+
<canvas ref="canvas" v-if="!errorMessage" />
8+
</div>
9+
</template>
10+
11+
<script setup lang="ts">
12+
import { getTrialData } from '@/plugins/idb'
13+
import type { CellPlus, TrialPlus } from '@/plugins/types/client'
14+
import { getColumnLabel, getRowLabel } from '@/plugins/util'
15+
16+
import canvasSize from 'canvas-size'
17+
import { useI18n } from 'vue-i18n'
18+
19+
const canvas = useTemplateRef('canvas')
20+
const loading = ref(false)
21+
const errorMessage = ref<string | undefined>()
22+
23+
const { n } = useI18n()
24+
25+
const compProps = defineProps<{
26+
trial: TrialPlus
27+
}>()
28+
29+
let ctx: CanvasRenderingContext2D | null | undefined = undefined
30+
const config = {
31+
padding: 10,
32+
cellWidth: 0,
33+
cellHeight: 0,
34+
totalWidth: 0,
35+
totalHeight: 0,
36+
paddingLeft: 0,
37+
paddingTop: 0,
38+
}
39+
40+
function init () {
41+
const canv = canvas.value
42+
if (canv) {
43+
canvasSize.maxArea({
44+
max: 20_000,
45+
min: 1,
46+
step: 100,
47+
useWorker: true,
48+
usePromise: true,
49+
}).then(result => {
50+
loading.value = false
51+
plot(result.width, result.height)
52+
})
53+
}
54+
}
55+
56+
async function plot (width: number, height: number) {
57+
const trialData = await getTrialData(compProps.trial.localId || '')
58+
59+
ctx = canvas.value?.getContext('2d', { alpha: false })
60+
61+
if (!ctx) {
62+
return
63+
}
64+
65+
ctx.translate(0.5, 0.5)
66+
ctx.textBaseline = 'middle'
67+
ctx.textAlign = 'center'
68+
ctx.font = '14px sans-serif'
69+
70+
const ctxx = ctx
71+
const canvass = canvas.value
72+
73+
if (!trialData || !ctxx || !canvass) {
74+
return
75+
}
76+
77+
let rowTextWidth = 0
78+
let columnTextWidth = 0
79+
let textWidth = 0
80+
let textHeight = 0
81+
82+
for (let y = 0; y < compProps.trial.layout.rows; y++) {
83+
const str = n(getRowLabel(compProps.trial.layout, y))
84+
rowTextWidth = Math.max(rowTextWidth, ctxx.measureText(str).width)
85+
}
86+
for (let x = 0; x < compProps.trial.layout.columns; x++) {
87+
const str = n(getColumnLabel(compProps.trial.layout, x))
88+
columnTextWidth = Math.max(columnTextWidth, ctxx.measureText(str).width)
89+
}
90+
91+
Object.values(trialData).forEach(td => {
92+
if (td && td.germplasm) {
93+
const dims = ctxx.measureText(td.displayName || '')
94+
95+
textHeight = Math.max(textHeight, dims.fontBoundingBoxAscent + dims.fontBoundingBoxDescent)
96+
textWidth = Math.max(textWidth, dims.width)
97+
}
98+
})
99+
100+
textWidth = Math.ceil(textWidth)
101+
textHeight = Math.ceil(textHeight)
102+
rowTextWidth = Math.ceil(rowTextWidth)
103+
columnTextWidth = Math.ceil(columnTextWidth)
104+
textWidth = Math.max(textWidth, columnTextWidth)
105+
106+
if (((textWidth + config.padding) * compProps.trial.layout.columns < width) && ((textHeight + config.padding) * compProps.trial.layout.rows) < height) {
107+
// Easy case, everything fits perfectly within the max size of the canvas
108+
config.totalWidth = ((textWidth + config.padding) * compProps.trial.layout.columns)
109+
config.totalHeight = ((textHeight + config.padding) * compProps.trial.layout.rows)
110+
config.cellWidth = config.totalWidth / compProps.trial.layout.columns
111+
config.cellHeight = config.totalHeight / compProps.trial.layout.rows
112+
config.paddingLeft = rowTextWidth + config.padding
113+
config.paddingTop = config.cellHeight
114+
115+
canvass.width = config.totalWidth + config.paddingLeft
116+
canvass.height = config.totalHeight + config.paddingTop
117+
canvass.style.width = `${config.totalWidth + config.paddingLeft}px`
118+
canvass.style.height = `${config.totalHeight + config.paddingTop}px`
119+
120+
nextTick(() => {
121+
ctx = canvas.value?.getContext('2d', { alpha: false })
122+
123+
if (ctx) {
124+
ctx.textBaseline = 'middle'
125+
ctx.textAlign = 'center'
126+
ctx.font = '14px sans-serif'
127+
128+
plotCells(ctx, trialData)
129+
}
130+
})
131+
} else {
132+
errorMessage.value = 'errorMessagePrintCanvasTooBig'
133+
}
134+
}
135+
136+
function plotCells (ctx: CanvasRenderingContext2D, trialData: { [key: string]: CellPlus }) {
137+
ctx.fillStyle = '#ffffff'
138+
ctx.fillRect(0, 0, config.totalWidth, config.totalHeight)
139+
140+
for (let r = 0; r < compProps.trial.layout.rows; r++) {
141+
ctx.fillStyle = '#ffffff'
142+
// Determine the background color
143+
if (r % 2 === 0) {
144+
ctx.fillStyle = '#f2f2f2'
145+
}
146+
147+
const str = n(getRowLabel(compProps.trial.layout, r))
148+
const y = r * config.cellHeight + config.paddingTop
149+
ctx.fillRect(0, y, config.cellWidth, config.cellHeight)
150+
ctx.fillStyle = 'black'
151+
ctx.fillText(str, config.paddingLeft / 2, y + config.cellHeight / 2)
152+
}
153+
154+
for (let c = 0; c < compProps.trial.layout.columns; c++) {
155+
ctx.fillStyle = '#ffffff'
156+
// Determine the background color
157+
if (c % 2 === 0) {
158+
ctx.fillStyle = '#f2f2f2'
159+
}
160+
161+
const str = n(getColumnLabel(compProps.trial.layout, c))
162+
const x = c * config.cellWidth + config.paddingLeft
163+
ctx.fillRect(x, 0, config.cellWidth, config.cellHeight)
164+
ctx.fillStyle = 'black'
165+
ctx.fillText(str, x + config.cellWidth / 2, config.paddingTop / 2)
166+
}
167+
168+
Object.values(trialData).forEach(td => {
169+
const row = td.row || 0
170+
const column = td.column || 0
171+
172+
let count = 0
173+
// Determine the background color
174+
if (row % 2 === 0) {
175+
count++
176+
}
177+
if (column % 2 === 0) {
178+
count++
179+
}
180+
switch (count) {
181+
case 0:
182+
ctx.fillStyle = '#ffffff'
183+
break
184+
case 1:
185+
ctx.fillStyle = '#f2f2f2'
186+
break
187+
default:
188+
ctx.fillStyle = '#e0e0e0'
189+
break
190+
}
191+
192+
const x = column * config.cellWidth + config.paddingLeft
193+
const y = row * config.cellHeight + config.paddingTop
194+
ctx.fillRect(x, y, config.cellWidth, config.cellHeight)
195+
ctx.fillStyle = 'black'
196+
ctx.fillText(td.displayName || '', x + config.cellWidth / 2, y + config.cellHeight / 2)
197+
})
198+
199+
ctx.strokeRect(0, 0, config.totalWidth + config.paddingLeft - 1, config.totalHeight + config.paddingTop - 1)
200+
}
201+
202+
onMounted(() => {
203+
errorMessage.value = undefined
204+
loading.value = true
205+
init()
206+
})
207+
</script>

src/components/setup/CornerPointsMap.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div v-if="corners">
33
<p>{{ $t('pageTrialLayoutCornersText') }}</p>
44

5-
<v-switch v-model="cornersEnabled" color="primary" :label="$t('formLabelUseCorners')" />
5+
<v-switch v-model="cornersEnabled" hide-details color="primary" :label="$t('formLabelUseCorners')" />
66

77
<div v-show="cornersEnabled && corners">
88
<v-row class="mb-5" v-if="corners && cornerLabels">

src/components/setup/LayoutMarkers.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div>
33
<p>{{ $t('pageTrialLayoutMarkersText') }}</p>
44

5-
<v-switch v-model="markersEnabled" color="primary" :label="$t('formLabelUseMarkers')" />
5+
<v-switch v-model="markersEnabled" hide-details color="primary" :label="$t('formLabelUseMarkers')" />
66

77
<div v-show="markersEnabled && markers">
88
<v-row v-if="markers">

0 commit comments

Comments
 (0)