Skip to content

Commit

Permalink
Merge pull request #59 from earthrise-media/canvas-rendering-for-d3-c…
Browse files Browse the repository at this point in the history
…omponents

Canvas rendering for d3 components
  • Loading branch information
Martin Bernard authored Nov 27, 2023
2 parents b9635e6 + 87d50a3 commit d1f74ef
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 109 deletions.
155 changes: 105 additions & 50 deletions vacs-map-app/src/components/DistributionPlot.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
<template>
<div class="svg-wrapper" ref="wrapperRef">
<svg>
<line
v-for="cell in gridCells"
:key="cell.id"
:x1="xScale(0)"
:x2="xScale(1)"
:y1="yScale(cell.val)"
:y2="yScale(cell.val)"
:stroke="getCellColor(cell.val)"
stroke-width="0.05"
stroke-opacity="1"
/>
</svg>
<div class="chart-wrapper" ref="wrapperRef">
<canvas ref="canvasRef"/>
</div>
</template>

<script setup>
import * as d3 from 'd3'
import { useResizeObserver } from '@vueuse/core'
import { computed, toRefs, ref, onMounted } from 'vue'
import { computed, toRefs, ref, onMounted, watch } from 'vue'
import { useFiltersStore } from '@/stores/filters'
import { useCropYieldsStore } from '@/stores/cropYields'
import { storeToRefs } from 'pinia'
Expand All @@ -38,37 +26,28 @@ const filtersStore = useFiltersStore()
const { selectedMetric, selectedCrop, availableModels, availableCrops } = storeToRefs(filtersStore)
const { data: cropYieldsData } = storeToRefs(cropYieldsStore)
const canvasRef = ref(null);
const context = ref(null);
const wrapperRef = ref(null)
const width = ref(0)
const height = ref(0)
useResizeObserver(wrapperRef, ([entry]) => {
width.value = entry.contentRect.width
height.value = entry.contentRect.height
})
const margin = computed(() => {
return height.value * 0.2
canvasRef.value.width = width.value;
canvasRef.value.height = height.value;
draw();
})
const columnName = computed(() => {
return [selectedMetric.value, selectedCrop.value, scenario.value].join('_')
})
const metaExtent = computed(() => {
// want to get extent across all scenarios and included crops so that comparisons are more useful
const extents = []
availableModels.value.forEach((s) => {
availableCrops.value.forEach((c) => {
const column = [selectedMetric.value, c, s].join('_')
extents.push(cropYieldsStore.getExtent(column))
})
})
return [d3.min(extents.map((d) => d[0])), d3.min(extents.map((d) => d[1]))]
})
const colorExtent = computed(() => {
// want to get extent across all scenarios and included crops so that comparisons are more useful
const cropExtent = computed(() => {
// want to get extent across all scenarios for selected crop
const extents = []
availableModels.value.forEach((s) => {
const column = [selectedMetric.value, selectedCrop.value, s].join('_')
Expand All @@ -80,51 +59,127 @@ const colorExtent = computed(() => {
const gridCells = computed(() => {
if (!cropYieldsData.value) return
return cropYieldsData.value.map((row) => {
const val = row[columnName.value];
return {
id: row.id,
val: row[columnName.value]
val,
x: xScale.value(val),
fill: getCellColor(val)
}
})
}).filter(d => !!d.val)
})
const yScale = computed(() => {
const xScale = computed(() => {
return d3
.scaleLinear()
.domain(metaExtent.value)
.range([height.value - margin.value, margin.value])
// .clamp(true);
})
const xScale = computed(() => {
return d3.scaleLinear().domain([0, 1]).range([0, width.value])
// keep 0 values centered on spectrum
.domain([cropExtent.value[0], 0, cropExtent.value[1]])
.range([0, width.value/2, width.value])
// .clamp(true);
})
const getCellColor = (value) => {
if (!value) return 'transparent'
const scale = d3
.scaleLinear()
.domain([colorExtent.value[0], 0, colorExtent.value[1]])
.domain([cropExtent.value[0], 0, cropExtent.value[1]])
.range([divergingScheme.min, divergingScheme.center, divergingScheme.max])
.clamp(true)
return scale(value)
}
// from D3 beeswarm example
const dodge = (data, {radius = 1, x = d => d} = {}) => {
const radius2 = radius ** 2;
const circles = data.map(d => ({x: x(d), data: d})).sort((a, b) => a.x - b.x);
const epsilon = 1e-3;
let head = null, tail = null;
// Returns true if circle ⟨x,y⟩ intersects with any circle in the queue.
function intersects(x, y) {
let a = head;
while (a) {
if (radius2 - epsilon > (a.x - x) ** 2 + (a.y - y) ** 2) {
return true;
}
a = a.next;
}
return false;
}
// Place each circle sequentially.
for (const b of circles) {
// Remove circles from the queue that can’t intersect the new circle b.
while (head && head.x < b.x - radius2) head = head.next;
// Choose the minimum non-intersecting tangent.
if (intersects(b.x, b.y = 0)) {
let a = head;
b.y = Infinity;
do {
let y1 = a.y + Math.sqrt(radius2 - (a.x - b.x) ** 2);
let y2 = a.y - Math.sqrt(radius2 - (a.x - b.x) ** 2);
if (Math.abs(y1) < Math.abs(b.y) && !intersects(b.x, y1)) b.y = y1;
if (Math.abs(y2) < Math.abs(b.y) && !intersects(b.x, y2)) b.y = y2;
a = a.next;
} while (a);
}
// Add b to the queue.
b.next = null;
if (head === null) head = tail = b;
else tail = tail.next = b;
}
return circles;
}
const draw = () => {
if (!context.value) return;
context.value.save();
//clear old canvas
context.value.clearRect(0, 0, width.value, height.value);
gridCells.value?.forEach(cell => {
context.value.fillStyle = cell.fill;
context.value.fillRect(cell.x, 0, 0.1, height.value);
})
// draw dots as swarm plot -- seems too slow
// const grid = dodge(gridCells.value, {radius: 1, x: d => d.x});
// //draw grid dots
// grid.forEach(cell => {
// context.value.fillStyle = cell.data.fill;
// context.value.fillRect(cell.x, height.value*.3 + cell.y, 2, 2);
// })
context.value.restore();
}
onMounted(() => {
width.value = wrapperRef.value.clientWidth
height.value = wrapperRef.value.clientHeight
canvasRef.value.width = width.value
canvasRef.value.height = height.value
context.value = canvasRef.value?.getContext('2d')
draw();
})
watch(selectedCrop, () => {
draw();
})
</script>
<style scoped>
.svg-wrapper {
.chart-wrapper {
height: 100%;
width: 100%;
padding: 1rem;
}
svg {
width: 100%;
height: 100%;
}
</style>
2 changes: 1 addition & 1 deletion vacs-map-app/src/components/ExploreSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const getCropsByGroup = (group) => {
width: 100%;
height: 40%;
display: flex;
flex-direction: row;
flex-direction: column;
gap: 1rem;
}
</style>
Loading

0 comments on commit d1f74ef

Please sign in to comment.