diff --git a/apps/client/README.md b/apps/client/README.md index ed41ee08..cf23452d 100644 --- a/apps/client/README.md +++ b/apps/client/README.md @@ -1,6 +1,6 @@ [![Netlify Status](https://api.netlify.com/api/v1/badges/d889955c-3ff6-4443-b1df-b7d740ea1cfc/deploy-status)](https://app.netlify.com/sites/bright-pika-7e11db/deploys) -# aardvark +# Loon This template should help get you started developing with Vue 3 in Vite. diff --git a/apps/client/index.html b/apps/client/index.html index 00e1059f..8647e2bc 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -4,7 +4,7 @@ - Aardvark + Loon
diff --git a/apps/client/package-lock.json b/apps/client/package-lock.json index 4abea753..51e1a0e7 100644 --- a/apps/client/package-lock.json +++ b/apps/client/package-lock.json @@ -1,11 +1,11 @@ { - "name": "aardvark", + "name": "loon", "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "aardvark", + "name": "loon", "version": "0.0.0", "dependencies": { "@deck.gl/core": "8.9.30", diff --git a/apps/client/package.json b/apps/client/package.json index dd91877a..f20970b4 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,5 +1,5 @@ { - "name": "aardvark", + "name": "loon", "version": "0.0.0", "private": true, "scripts": { @@ -8,7 +8,8 @@ "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json,vue}\"" }, "dependencies": { "@deck.gl/core": "8.9.30", diff --git a/apps/client/src/components/AggregateLineChart.vue b/apps/client/src/components/AggregateLineChart.vue index ee22a67c..6946331b 100644 --- a/apps/client/src/components/AggregateLineChart.vue +++ b/apps/client/src/components/AggregateLineChart.vue @@ -129,6 +129,18 @@ const areaGen = computed(() => { ); }); +const areaGenHighlighted = computed(() => { + return area() + .x((aggPoint) => scaleX.value(aggPoint.time)) + .y0((aggPoint) => + scaleY.value(aggPoint.value + temp.value * aggPoint.count) + ) + .y1((aggPoint) => + scaleY.value(aggPoint.value - temp.value * aggPoint.count) + ) + .defined((aggPoint) => aggPoint.highlighted ?? false); +}); + const lineGen = computed(() => { return line() .x((aggPoint) => scaleX.value(aggPoint.time)) @@ -522,7 +534,9 @@ const otherUnmuted = computed(() => { + + { stroke-width: 1px; opacity: 0.6; } + +.highlighted.agg-line { + stroke-width: 3px; + opacity: 1; +} + +.filtered.agg-line { + opacity: 0 !important; +} .selected.agg-line { stroke-width: 4px; } diff --git a/apps/client/src/components/ImageViewer.vue b/apps/client/src/components/ImageViewer.vue index 0bffb2ab..9e8fab32 100644 --- a/apps/client/src/components/ImageViewer.vue +++ b/apps/client/src/components/ImageViewer.vue @@ -21,7 +21,6 @@ import Pool from '../util/Pool'; import { useLooneageViewStore } from '@/stores/componentStores/looneageViewStore'; import { useGlobalSettings } from '@/stores/componentStores/globalSettingsStore'; - import { loadOmeTiff, getChannelStats, @@ -42,6 +41,7 @@ import { TripsLayer } from '@deck.gl/geo-layers'; import { format } from 'd3-format'; import colors from '@/util/colors'; import { useConfigStore } from '@/stores/misc/configStore'; +import { useMosaicSelectionStore } from '@/stores/dataStores/mosaicSelectionStore'; const cellMetaData = useCellMetaData(); const globalSettings = useGlobalSettings(); @@ -57,6 +57,10 @@ const { currentLocationMetadata } = storeToRefs(datasetSelectionStore); const { contrastLimitSlider } = storeToRefs(imageViewerStoreUntrracked); const eventBusStore = useEventBusStore(); const looneageViewStore = useLooneageViewStore(); +const mosaicSelectionStore = useMosaicSelectionStore(); +const { highlightedCellIds, unfilteredTrackIds } = + storeToRefs(mosaicSelectionStore); + const deckGlContainer = ref(null); const { width: containerWidth, height: containerHeight } = useElementSize(deckGlContainer); @@ -66,6 +70,27 @@ const contrastLimit = computed<[number, number][]>(() => { return [[contrastLimitSlider.value.min, contrastLimitSlider.value.max]]; }); +function _determineSelectedOrFiltered(trackId: string): { + selected: boolean; + filtered: boolean; +} { + const frame = imageViewerStore.frameNumber; + const location = currentLocationMetadata.value?.id; + let selected = true; + if (frame && location && highlightedCellIds.value) { + // Generate Unique String to compare against list + const unique_string = `${trackId}_${frame}_${location}`; + selected = highlightedCellIds.value.includes(unique_string); + } + + return { + selected, + filtered: unfilteredTrackIds.value + ? !unfilteredTrackIds.value.includes(trackId) + : false, + }; +} + let deckgl: any | null = null; onMounted(() => { deckgl = new Deck({ @@ -121,7 +146,6 @@ onMounted(() => { // onLoad: () => console.log('onLoad'), getTooltip: ({ object }) => { - console.log(object); if (!object) return null; let { id, frame } = object.properties; if (id == null) return null; @@ -218,7 +242,7 @@ function createSegmentationsLayer(): typeof GeoJsonLayer { ), lineWidthUnits: 'pixels', id: 'segmentations', - opacity: 0.4, + opacity: 1, stroked: true, filled: true, getFillColor: (info) => { @@ -242,6 +266,17 @@ function createSegmentationsLayer(): typeof GeoJsonLayer { ) { return colors.hovered.rgb; } + + const { selected, filtered } = _determineSelectedOrFiltered( + info.properties?.id?.toString() + ); + // Removes outline + if (filtered) { + return [0, 0, 0]; + } + if (selected) { + return colors.highlightedBoundary.rgb; + } return colors.unselectedBoundary.rgb; }, getLineWidth: (info) => { @@ -251,15 +286,21 @@ function createSegmentationsLayer(): typeof GeoJsonLayer { ) { return 3; } - return 2; + const { selected, filtered } = _determineSelectedOrFiltered( + info.properties?.id?.toString() + ); + if (selected) { + return 2.5; + } + return 1.5; }, pickable: true, onHover: onHover, onClick: onClick, updateTriggers: { - getFillColor: dataPointSelectionUntrracked.hoveredTrackId, - getLineColor: dataPointSelection.selectedTrackId, - getLineWidth: dataPointSelection.selectedTrackId, + getFillColor: [dataPointSelectionUntrracked.hoveredTrackId], + getLineColor: [dataPointSelection.selectedTrackId], + getLineWidth: [dataPointSelection.selectedTrackId], }, }); } @@ -370,23 +411,6 @@ function addSegmentsFromTrack( }); return accumChildPositions; - - // for (let i = 0; i < track.cells.length - 1; i++) { - // const start = track.cells[0]; - // const end = track.cells[track.cells.length - 1]; - // if (cellMetaData.getFrame(end) >= imageViewerStore.frameNumber) { - // return; - // } - // segments.push({ - // trackId: track.trackId, - // from: cellMetaData.getPosition(start), - // to: cellMetaData.getPosition(end), - // }); - // // } - // if (!track.children) return; - // for (let child of track.children) { - // addSegmentsFromTrack(child, segments); - // } } function createLineageLayer(): LineLayer { @@ -453,17 +477,24 @@ function createCenterPointLayer(): ScatterplotLayer { if (d.trackId === dataPointSelection.selectedTrackId) { return globalSettings.normalizedSelectedRgb; } + + // const { filtered } = _determineSelectedOrFiltered(d.trackId); + + // if (filtered) { + // return [0, 0, 0, 0]; + // } + return [228, 26, 28]; }, getStrokeWidth: 1, updateTriggers: { getFillColor: { - selected: dataPointSelection.selectedTrackId, - hovered: dataPointSelectionUntrracked.hoveredTrackId, + selected: [dataPointSelection.selectedTrackId], + hovered: [dataPointSelectionUntrracked.hoveredTrackId], }, getLineColor: { - selected: dataPointSelection.selectedTrackId, - hovered: dataPointSelectionUntrracked.hoveredTrackId, + selected: [dataPointSelection.selectedTrackId], + hovered: [dataPointSelectionUntrracked.hoveredTrackId], }, }, }); @@ -533,7 +564,7 @@ function createTrajectoryGhostLayer(): TripsLayer { const imageLayer = ref(); function renderDeckGL(): void { if (deckgl == null) return; - if (!cellMetaData.dataInitialized || cellMetaData.selectedLineage == null) { + if (!cellMetaData.dataInitialized) { renderLoadingDeckGL(); return; } @@ -652,6 +683,8 @@ watch(currentTrackArray, renderDeckGL); watch(dataPointSelection.$state, renderDeckGL); watch(imageViewerStore.$state, renderDeckGL); watch(contrastLimitSlider, renderDeckGL); +watch(highlightedCellIds, renderDeckGL); +watch(unfilteredTrackIds, renderDeckGL); function clearSelection() { dataPointSelection.selectedTrackId = null; diff --git a/apps/client/src/components/conditionSelector/ConditionChart.vue b/apps/client/src/components/conditionSelector/ConditionChart.vue new file mode 100644 index 00000000..6a790f9f --- /dev/null +++ b/apps/client/src/components/conditionSelector/ConditionChart.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/apps/client/src/components/conditionSelector/ConditionSelector.vue b/apps/client/src/components/conditionSelector/ConditionSelector.vue new file mode 100644 index 00000000..718d7227 --- /dev/null +++ b/apps/client/src/components/conditionSelector/ConditionSelector.vue @@ -0,0 +1,490 @@ + + + + + diff --git a/apps/client/src/components/conditionSelector/ConditionSelectorCompareView.vue b/apps/client/src/components/conditionSelector/ConditionSelectorCompareView.vue new file mode 100644 index 00000000..e9aa11f8 --- /dev/null +++ b/apps/client/src/components/conditionSelector/ConditionSelectorCompareView.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/apps/client/src/components/conditionSelector/ConditionSelectorDropdown.vue b/apps/client/src/components/conditionSelector/ConditionSelectorDropdown.vue new file mode 100644 index 00000000..8bcf2dd8 --- /dev/null +++ b/apps/client/src/components/conditionSelector/ConditionSelectorDropdown.vue @@ -0,0 +1,61 @@ + + + + diff --git a/apps/client/src/components/conditionSelector/duckdb_findings.txt b/apps/client/src/components/conditionSelector/duckdb_findings.txt new file mode 100644 index 00000000..af78beb5 --- /dev/null +++ b/apps/client/src/components/conditionSelector/duckdb_findings.txt @@ -0,0 +1,44 @@ +DuckDB Findings + + +1. Can't use type:json if we have a big int, JSON can't serialize it (this is using coordinator.query()) +2. Can't do generic select by statement and pass data into VG plot +3. Even if we do use a different plotting structure, cant seem to figure out how to grab results from query functionality + -> its using apache arrow -- we can probably install something to parse this if necessary + + + Queries for quantile -- finds average mass of every tracking id. takes the quantile of the average mass: + + + const res = await vg.coordinator().query(` + WITH cte AS ( + SELECT approx_quantile(avg_mass, 0.5) as quantile_5 + FROM ( + SELECT avg("Dry Mass (pg)") as avg_mass + FROM ${currentExperimentMetadata?.value?.name}_composite_experiment_cell_metadata + GROUP BY tracking_id, "Location" + ) + ) + SELECT + CAST("Tracking ID" AS VARCHAR), + cte.quantile_5 + FROM ${currentExperimentMetadata?.value?.name}_composite_experiment_cell_metadata + CROSS JOIN cte + ORDER BY ABS("Dry Mass (pg)" - cte.quantile_5) + LIMIT 1;`, + {'type':'json'}) + console.log(res); + + + + + + SELECT tracking_id, location, avg("Dry Mass (pg)") from table group by tracking_id, location; + + + +const $test_selection = vg.Selection.intersect(); +$test_selection.update({ + source:'test_source', + predicate:`"Tracking ID" IN (SELECT tracking_id FROM ${currentExperimentMetadata?.value?.name}_composite_experiment_cell_metadata_aggregate WHERE track_length between 0 and 100 )` +}) \ No newline at end of file diff --git a/apps/client/src/components/exemplarView/ExemplarView.vue b/apps/client/src/components/exemplarView/ExemplarView.vue new file mode 100644 index 00000000..bb31a5c6 --- /dev/null +++ b/apps/client/src/components/exemplarView/ExemplarView.vue @@ -0,0 +1,102 @@ + + + diff --git a/apps/client/src/components/globalSettings/DatasetSelector.vue b/apps/client/src/components/globalSettings/DatasetSelector.vue index dbc79a12..c81758c4 100644 --- a/apps/client/src/components/globalSettings/DatasetSelector.vue +++ b/apps/client/src/components/globalSettings/DatasetSelector.vue @@ -6,7 +6,8 @@ import { useDatasetSelectionTrrackedStore } from '@/stores/dataStores/datasetSel import { useDatasetSelectionStore } from '@/stores/dataStores/datasetSelectionUntrrackedStore'; import { useSelectionStore } from '@/stores/interactionStores/selectionStore'; -import { useFilterStore } from '@/stores/componentStores/filterStore'; +import { useMosaicSelectionStore } from '@/stores/dataStores/mosaicSelectionStore'; +import { useConditionSelectorStore } from '@/stores/componentStores/conditionSelectorStore'; const globalSettings = useGlobalSettings(); const datasetSelectionStore = useDatasetSelectionStore(); @@ -14,7 +15,8 @@ const datasetSelectionTrrackedStore = useDatasetSelectionTrrackedStore(); const $q = useQuasar(); const selectionStore = useSelectionStore(); -const filterStore = useFilterStore(); +const mosaicSelectionStore = useMosaicSelectionStore(); +const conditionSelectorStore = useConditionSelectorStore(); watch( () => datasetSelectionStore.fetchingTabularData, @@ -30,9 +32,6 @@ watch( ); function onClickLocation(location: any) { - //console.log('clicked location: ', location); - selectionStore.clearAllSelections(); - filterStore.clearAllFilters(); datasetSelectionStore.selectImagingLocation(location); } @@ -48,28 +47,31 @@ const shortExpName = computed(() => { }); function onSelectExperiment() { - selectionStore.clearAllSelections(); - filterStore.clearAllFilters(); + selectionStore.resetState(); + mosaicSelectionStore.resetState(); + conditionSelectorStore.resetState(); } - + left class="q-pa-none q-mr-xs flex-grow-0" > - + {{ filter.plotName }} +
{{ filter.subName }}
@@ -164,8 +217,7 @@ const mutedTextClass = computed(() => label="Cell Attributes" > - + - + + + + + + +
diff --git a/apps/client/src/components/plotSelector/PlotSelector.vue b/apps/client/src/components/plotSelector/PlotSelector.vue new file mode 100644 index 00000000..9b3a9904 --- /dev/null +++ b/apps/client/src/components/plotSelector/PlotSelector.vue @@ -0,0 +1,383 @@ + + + + diff --git a/apps/client/src/components/plotSelector/UnivariateCellPlot.vue b/apps/client/src/components/plotSelector/UnivariateCellPlot.vue new file mode 100644 index 00000000..08589cc3 --- /dev/null +++ b/apps/client/src/components/plotSelector/UnivariateCellPlot.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/apps/client/src/components/plotSelector/aggregateFunctions.ts b/apps/client/src/components/plotSelector/aggregateFunctions.ts new file mode 100644 index 00000000..b74f15fc --- /dev/null +++ b/apps/client/src/components/plotSelector/aggregateFunctions.ts @@ -0,0 +1,186 @@ +export interface AttributeSelection { + label: string; + type: 'existing_attribute' | 'numerical'; + max?: number; + min?: number; + step?: number; +} + +export type AggregateFunction = + | BasicAggregateFunction + | StandardAggregateFunction + | CustomAggregateFunction; + +export interface CustomAggregateFunction extends BasicAggregateFunction { + customQuery: string; +} + +export interface StandardAggregateFunction extends BasicAggregateFunction { + selections: Record; +} + +export interface BasicAggregateFunction { + functionName: string; + description: string; +} + +export function isStandardAggregateFunction( + aggFunction: AggregateFunction +): aggFunction is StandardAggregateFunction { + return 'selections' in aggFunction; +} + +export function isCustomAggregateFunction( + aggFunction: AggregateFunction +): aggFunction is CustomAggregateFunction { + return 'customQuery' in aggFunction; +} +export const aggregateFunctions: Record = { + Sum: { + functionName: 'SUM', + description: 'Sum of selected attribute over each track.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + }, + }, + Average: { + functionName: 'AVG', + description: 'Average of selected attribute over each track.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + }, + }, + 'Track Length': { + functionName: 'COUNT', + description: 'Number of cells in each track.', + }, + Minimum: { + functionName: 'MIN', + description: 'Minimum of selected attribute across each track.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + }, + }, + Maximum: { + functionName: 'MAX', + description: 'Maximum of selected attribute across each track.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + }, + }, + 'Median Absolute Deviation': { + functionName: 'MAD', + description: 'Median of all absolute deviations.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + }, + }, + 'Continuous Quantile': { + functionName: 'QUANTILE_CONT', + description: 'The pth continuous quantile of the selected attribute.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + var1: { + label: 'Position', + type: 'numerical', + max: 1, + min: 0, + step: 0.1, + }, + }, + }, + 'Linear Regression Slope': { + functionName: 'REGR_SLOPE', + description: + 'Slope of the linear regression line between the two selected attributes.', + selections: { + attr1: { + label: 'Attribute', + type: 'existing_attribute', + }, + attr2: { + label: 'Attribute', + type: 'existing_attribute', + }, + }, + }, + /* + Custom query must do the following: + - Any string replacement needed uses the {item} syntax. This is the same syntax as typescripts syntax without the "$" + - The result of the query must return a column aliased as the function name + - The result of the query must return the tracking id aliased as "tracking_id" + - There must only be one result per tracking id. + */ + 'Initial Mass': { + functionName: 'init_mass', + description: + 'The adjusted initial mass determined by linear regression.', + customQuery: ` + SELECT + MIN(t1."{timeColumn}")*regr_line.slope + regr_line.intercept as init_mass, + t1."{idColumn}" as tracking_id + FROM {compTable} as t1 + LEFT JOIN ( + SELECT + regr_slope("{massColumn}", "{timeColumn}") as slope, + regr_intercept("{massColumn}", "{timeColumn}") as intercept, + "{idColumn}" as tracking_id + FROM {compTable} + GROUP BY "{idColumn}" + ) as regr_line + ON t1."{idColumn}" = regr_line.tracking_id + GROUP BY t1."{idColumn}", regr_line.slope, regr_line.intercept + `, + }, + 'Exponential Growth Rate Constant': { + functionName: 'exp_growth_rate_constant', + description: + 'The slope of the linear regression line divided by the initial mass.', + customQuery: ` + SELECT + regr_line.slope/(MIN(t1."{timeColumn}")*regr_line.slope + regr_line.intercept) as exp_growth_rate_constant, + t1."{idColumn}" as tracking_id + FROM {compTable} as t1 + LEFT JOIN ( + SELECT + regr_slope("{massColumn}", "{timeColumn}") as slope, + regr_intercept("{massColumn}", "{timeColumn}") as intercept, + "{idColumn}" as tracking_id + FROM {compTable} + GROUP BY "{idColumn}" + ) as regr_line + ON t1."{idColumn}" = regr_line.tracking_id + GROUP BY t1."{idColumn}", regr_line.slope, regr_line.intercept + `, + }, + 'Growth Rate': { + functionName: 'growth_rate', + description: + 'The slope of the linear regression line where time is the independent variable and mass is the dependent variable.', + customQuery: ` + SELECT + regr_slope("{massColumn}", "{timeColumn}") as growth_rate, + "{idColumn}" as tracking_id + FROM {compTable} + GROUP BY "{idColumn}" + `, + }, +}; diff --git a/apps/client/src/components/upload/LoadingProgress.vue b/apps/client/src/components/upload/LoadingProgress.vue index 73b914f1..15ef542d 100644 --- a/apps/client/src/components/upload/LoadingProgress.vue +++ b/apps/client/src/components/upload/LoadingProgress.vue @@ -1,8 +1,6 @@