-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #127 from LCOGT/feature/histogram_slider
Feature/histogram slider
- Loading branch information
Showing
9 changed files
with
269 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
<script setup> | ||
import { ref, defineProps, defineEmits, computed, watch } from 'vue' | ||
// This component draws a sparkline histogram with range slider controls on top of it. | ||
// It has two coordinate systems - sliderRange which is a linear system of bins for the | ||
// histogram passed in, with bin middles defined by a same-sized array bins, and scaleRange | ||
// which is the actual lower/upper point values in pixel space. You can get better granularity | ||
// on changing scaleRange with the number fields, since that can change to any value in the range | ||
// vs using the histogram slider which has steps according to the highly non-linear bins used | ||
// to generate the histogram. | ||
const props = defineProps({ | ||
histogram: { | ||
type: Array, | ||
required: true | ||
}, | ||
bins: { | ||
type: Array, | ||
required: true | ||
}, | ||
maxValue: { | ||
type: Number, | ||
required: false, | ||
default: 65500 | ||
}, | ||
zMin: { | ||
type: Number, | ||
required: false, | ||
default: 0 | ||
}, | ||
zMax: { | ||
type: Number, | ||
required: false, | ||
default: 65500 | ||
} | ||
}) | ||
const emit = defineEmits(['updateScaling']) | ||
const scaleRange = ref([props.bins[0], props.bins[props.bins.length-1]]) | ||
const sliderRange = ref([0, props.bins.length-1]) | ||
const selectedColor = 'rgb(20,182,246)' | ||
const backgroundColor = 'rgb(204,208,211)' | ||
const gradient = computed(() => { | ||
// This function computes a "gradient" for the histogram sparkline, showing the selected region as highlighted | ||
let gradientArray = [] | ||
for (let i = 0; i < props.bins.length; i++) { | ||
if (i > (sliderRange.value[0]+1) && i < (sliderRange.value[1]-1)) { | ||
gradientArray.push(selectedColor) | ||
} | ||
else { | ||
gradientArray.push(backgroundColor) | ||
} | ||
} | ||
return gradientArray | ||
}) | ||
function sliderToBinValue(sliderValue) { | ||
// This converts from sliderRange coordinates to scaleRange coordinates | ||
if (props.bins.length >= sliderValue) { | ||
return props.bins[sliderValue] | ||
} | ||
} | ||
function labelSliderToScaleValue(sliderValue) { | ||
// This returns the current scaleRange value given a sliderValue. It doesn't convert and show the sliderValue | ||
// so that we can correctly show what the scaleRange is if it is modified in the number fields. | ||
if (sliderValue == sliderRange.value[0]) { | ||
return scaleRange.value[0] | ||
} | ||
return scaleRange.value[1] | ||
} | ||
function scaleToSliderValue(scaleValue) { | ||
// This converts from scaleRange to the nearest sliderRange value, which allows us to map from scale space to | ||
// slider space when the user makes a change to the scaleRange directly with the number fields | ||
if (Math.abs(scaleValue - props.zMin) > Math.abs(scaleValue - props.zMax)) { | ||
// Closer to the zMax, so iterate in reverse to find a match sooner | ||
let previousDistance = Math.abs(scaleValue - props.bins[props.bins.length-1]) | ||
for (let i = props.bins.length-2; i >= 0; i--) { | ||
let distance = Math.abs(scaleValue - props.bins[i]) | ||
if (distance > previousDistance) { | ||
return i + 1 | ||
} | ||
else { | ||
previousDistance = distance | ||
} | ||
} | ||
return 0 | ||
} | ||
else { | ||
// Closer to zMin, so iterate in the natural order | ||
let previousDistance = Math.abs(scaleValue - props.bins[0]) | ||
for (let i = 1; i < props.bins.length; i++) { | ||
let distance = Math.abs(scaleValue - props.bins[i]) | ||
if (distance > previousDistance) { | ||
return i - 1 | ||
} | ||
else { | ||
previousDistance = distance | ||
} | ||
} | ||
return props.bins.length - 1 | ||
} | ||
} | ||
function updateScaleRange() { | ||
// This is called when the range slider control is moved | ||
scaleRange.value[0] = sliderToBinValue(sliderRange.value[0]) | ||
scaleRange.value[1] = sliderToBinValue(sliderRange.value[1]) | ||
emit('updateScaling', scaleRange.value[0], scaleRange.value[1]) | ||
} | ||
function updateLowerScale(value) { | ||
// This is called when a change is made on the lower point number control | ||
scaleRange.value = [Number(value), scaleRange.value[1]] | ||
sliderRange.value[0] = scaleToSliderValue(Number(value)) | ||
emit('updateScaling', scaleRange.value[0], scaleRange.value[1]) | ||
} | ||
function updateUpperScale(value) { | ||
// This is called when a change is made on the upper point number control | ||
scaleRange.value = [scaleRange.value[0], Number(value)] | ||
sliderRange.value[1] = scaleToSliderValue(Number(value)) | ||
emit('updateScaling', scaleRange.value[0], scaleRange.value[1]) | ||
} | ||
function zScaleImage() { | ||
// This is called to reset the ranges to the zMin/zMax | ||
scaleRange.value = [props.zMin, props.zMax] | ||
sliderRange.value = [scaleToSliderValue(props.zMin), scaleToSliderValue(props.zMax)] | ||
emit('updateScaling', scaleRange.value[0], scaleRange.value[1]) | ||
} | ||
watch( | ||
() => props.zMax, () => { | ||
zScaleImage() | ||
} | ||
) | ||
</script> | ||
<template> | ||
<v-row class="ml-4"> | ||
<v-col cols="4"> | ||
<v-text-field | ||
v-model="scaleRange[0]" | ||
:min="0" | ||
label="lower point" | ||
density="compact" | ||
type="number" | ||
variant="outlined" | ||
step="10" | ||
hide-details | ||
@update:modelValue="updateLowerScale"> | ||
</v-text-field> | ||
</v-col> | ||
<v-col cols="4"> | ||
<v-text-field | ||
v-model="scaleRange[1]" | ||
:max="props.maxValue" | ||
label="upper point" | ||
density="compact" | ||
type="number" | ||
variant="outlined" | ||
step="10" | ||
hide-details | ||
@update:modelValue="updateUpperScale"> | ||
</v-text-field> | ||
</v-col> | ||
<v-col cols="4"> | ||
<v-btn | ||
class="ml-2" | ||
variant="outlined" | ||
@click="zScaleImage" | ||
>Z-Scale</v-btn> | ||
</v-col> | ||
</v-row> | ||
<v-row class="ma-0"> | ||
<v-sheet class="stackSheet"> | ||
<v-sparkline | ||
smooth=true | ||
fill=true | ||
line-width="0.1" | ||
height="50" | ||
:gradient="gradient" | ||
gradient-direction='left' | ||
:model-value="props.histogram" | ||
auto-draw | ||
> | ||
</v-sparkline> | ||
<v-range-slider | ||
class="stackControls" | ||
v-model="sliderRange" | ||
step="1" | ||
track-size="0" | ||
:track-color="backgroundColor" | ||
:track-fill-color="selectedColor" | ||
:thumb-color="selectedColor" | ||
thumb-size="16" | ||
:max="props.bins.length-1" | ||
thumb-label="always" | ||
strict | ||
hide-details | ||
@update:modelValue="updateScaleRange" | ||
> | ||
<template v-slot:thumb-label="{ modelValue }"> | ||
{{ labelSliderToScaleValue(modelValue) }} | ||
</template> | ||
</v-range-slider> | ||
</v-sheet> | ||
</v-row> | ||
</template> | ||
<style scoped> | ||
.stackSheet { | ||
position: relative; | ||
width: 500px; | ||
margin-left: 10px; | ||
} | ||
.stackControls { | ||
position: absolute; | ||
bottom: -6px; | ||
left: 5px; | ||
width: 473px; | ||
} | ||
</style> |
Oops, something went wrong.