Skip to content

Commit 7944b88

Browse files
committed
Bubble plot default color by bubble size
1 parent a821bb0 commit 7944b88

File tree

5 files changed

+93
-31
lines changed

5 files changed

+93
-31
lines changed

smoosense-gui/src/components/common/NumericalColumnDropdown.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ import {
1616
SelectValue,
1717
} from '@/components/ui/select'
1818

19+
export interface ExtraOption {
20+
value: string
21+
label: string
22+
}
23+
1924
interface NumericalColumnDropdownProps {
2025
settingKey: keyof UIState
2126
label: string
2227
shouldInitialize?: boolean
2328
showStats?: boolean
29+
extraOptions?: ExtraOption[]
2430
}
2531

2632
type UIState = {
@@ -41,7 +47,8 @@ export default function NumericalColumnDropdown({
4147
settingKey,
4248
label,
4349
shouldInitialize = true,
44-
showStats = false
50+
showStats = false,
51+
extraOptions = []
4552
}: NumericalColumnDropdownProps) {
4653
const dispatch = useAppDispatch()
4754
const { isCategoricalColumns } = useIsCategoricalBulk()
@@ -68,16 +75,23 @@ export default function NumericalColumnDropdown({
6875
}
6976
}
7077

71-
// Auto-initialize if shouldInitialize is true
78+
// Auto-initialize:
79+
// - If shouldInitialize is true, initialize to first available column
80+
// - If extraOptions is provided, initialize to first extraOption
7281
useEffect(() => {
73-
if (shouldInitialize && availableColumns.length > 0 && !currentValue) {
82+
if (!currentValue) {
7483
const action = actionMap[settingKey]
7584
if (action) {
76-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
77-
dispatch((action as any)(availableColumns[0]))
85+
if (shouldInitialize && availableColumns.length > 0) {
86+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
87+
dispatch((action as any)(availableColumns[0]))
88+
} else if (extraOptions.length > 0) {
89+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
90+
dispatch((action as any)(extraOptions[0].value))
91+
}
7892
}
7993
}
80-
}, [shouldInitialize, availableColumns, currentValue, settingKey, dispatch])
94+
}, [shouldInitialize, availableColumns, currentValue, settingKey, dispatch, extraOptions])
8195

8296
const getPlaceholderText = () => {
8397
if (!isCategoricalColumns || !renderTypes) {
@@ -109,11 +123,20 @@ export default function NumericalColumnDropdown({
109123
disabled={isDisabled}
110124
>
111125
<SelectTrigger>
112-
<SelectValue placeholder={placeholderText} />
126+
<SelectValue placeholder={placeholderText}>
127+
{/* Show label for extra options, otherwise show value */}
128+
{extraOptions.find(opt => opt.value === currentValue)?.label ?? (currentValue === "" ? "-" : currentValue)}
129+
</SelectValue>
113130
</SelectTrigger>
114131
<SelectContent>
115132
{/* Null option */}
116133
<SelectItem value="-">-</SelectItem>
134+
{/* Extra options */}
135+
{extraOptions.map((option) => (
136+
<SelectItem key={option.value} value={option.value}>
137+
{option.label}
138+
</SelectItem>
139+
))}
117140
{availableColumns.map((column) => (
118141
<SelectItem key={column} value={column}>
119142
{column}

smoosense-gui/src/lib/features/bubblePlot/BubblePlotMoreControls.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import ColorScaleDropdown from '@/components/settings/ColorScaleDropdown'
99
import { useAppSelector, useAppDispatch } from '@/lib/hooks'
1010
import { setBubblePlotMaxMarkerSize, setBubblePlotMinMarkerSize, setBubblePlotOpacity, setBubblePlotMarkerSizeContrastRatio } from '@/lib/features/ui/uiSlice'
1111

12+
// Special value for coloring by bubble size (count)
13+
export const BUBBLE_SIZE_COLOR_VALUE = '__bubble_size__'
14+
15+
const BUBBLE_SIZE_EXTRA_OPTIONS = [
16+
{ value: BUBBLE_SIZE_COLOR_VALUE, label: 'Bubble Size' }
17+
]
18+
1219
function BubblePlotMoreControlsContent() {
1320
const dispatch = useAppDispatch()
1421
const bubblePlotMaxMarkerSize = useAppSelector((state) => state.ui.bubblePlotMaxMarkerSize)
@@ -27,6 +34,7 @@ function BubblePlotMoreControlsContent() {
2734
settingKey="bubblePlotColorColumn"
2835
label="Color by"
2936
shouldInitialize={false}
37+
extraOptions={BUBBLE_SIZE_EXTRA_OPTIONS}
3038
/>
3139

3240
<ColorScaleDropdown />

smoosense-gui/src/lib/features/bubblePlot/PlotlyBubblePlot.tsx

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import React, { useMemo, useCallback, useEffect, useState } from 'react'
44
import dynamic from 'next/dynamic'
55
import { useAppSelector, useAppDispatch } from '@/lib/hooks'
6-
import type { BubblePlotGroup } from './bubblePlotSlice'
6+
import type { BubblePlotGroup } from '@/lib/features/bubblePlot/bubblePlotSlice'
7+
import { BUBBLE_SIZE_COLOR_VALUE } from '@/lib/features/bubblePlot/BubblePlotMoreControls'
78
import type { PlotData, Layout, Config } from 'plotly.js'
89
import { usePlotlyLayout, usePlotlyConfig, usePlotlyColors } from '@/lib/utils/plotlyTheme'
910
import { setSamplingCondition } from '@/lib/features/viewing/viewingSlice'
@@ -84,7 +85,7 @@ const PlotlyBubblePlot = React.memo(function PlotlyBubblePlot({ data }: PlotlyBu
8485
marker.colorscale = colorScale
8586
marker.showscale = true
8687
marker.colorbar = {
87-
title: colorColumn,
88+
title: colorColumn === BUBBLE_SIZE_COLOR_VALUE ? 'Bubble Size' : colorColumn,
8889
thickness: 15,
8990
len: 0.5
9091
}
@@ -104,7 +105,7 @@ const PlotlyBubblePlot = React.memo(function PlotlyBubblePlot({ data }: PlotlyBu
104105
`<b>Y ${yColumn}:</b> ~%{y}<br>` +
105106
(breakdownColumn ? `<b>${breakdownColumn}:</b> ${item.name}<br>` : '') +
106107
`<b>Count:</b> %{customdata.count}<br>` +
107-
(hasColorValues ? `<b>${colorColumn}:</b> %{customdata.colorValue:.2f}<br>` : '') +
108+
(hasColorValues && colorColumn !== BUBBLE_SIZE_COLOR_VALUE ? `<b>${colorColumn}:</b> %{customdata.colorValue:.2f}<br>` : '') +
108109
'<extra></extra>',
109110
}
110111
return trace
@@ -117,10 +118,10 @@ const PlotlyBubblePlot = React.memo(function PlotlyBubblePlot({ data }: PlotlyBu
117118
}
118119
}, [data, xColumn, yColumn, breakdownColumn, colorColumn, colorScale, opacity, maxMarkerSize, minMarkerSize, markerSizeContrastRatio, colors.foreground])
119120

121+
// Get display name for color column
122+
const colorDisplayName = colorColumn === BUBBLE_SIZE_COLOR_VALUE ? 'Bubble Size' : colorColumn
123+
120124
const baseLayout = usePlotlyLayout({
121-
title: colorColumn ? `Color: ${colorColumn}` : undefined,
122-
xTitle: `X: ${xColumn}`,
123-
yTitle: `Y: ${yColumn}`,
124125
showLegend: breakdownColumn !== null
125126
})
126127

@@ -172,6 +173,18 @@ const PlotlyBubblePlot = React.memo(function PlotlyBubblePlot({ data }: PlotlyBu
172173
}
173174
}, [dispatch])
174175

176+
// Compute bubble stats for info line
177+
const bubbleStats = useMemo(() => {
178+
if (!data || data.length === 0) return { total: 0, minSize: 0, maxSize: 0 }
179+
180+
const allCounts = data.flatMap(group => group.customdata.map(c => c.count))
181+
const total = data.reduce((sum, group) => sum + group.customdata.length, 0)
182+
const minSize = allCounts.length > 0 ? Math.min(...allCounts) : 0
183+
const maxSize = allCounts.length > 0 ? Math.max(...allCounts) : 0
184+
185+
return { total, minSize, maxSize }
186+
}, [data])
187+
175188
// Don't render on server side
176189
if (!isClient) {
177190
return (
@@ -194,21 +207,34 @@ const PlotlyBubblePlot = React.memo(function PlotlyBubblePlot({ data }: PlotlyBu
194207
}
195208

196209
return (
197-
<div className="w-full h-full min-h-[400px]">
198-
<Plot
199-
data={plotData}
200-
layout={layout}
201-
config={config}
202-
style={{ width: '100%', height: '100%' }}
203-
useResizeHandler={true}
204-
onClick={handleClick}
205-
onSelected={handleSelected}
206-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
207-
onError={(error: any) => {
208-
console.error('Plotly error:', error)
209-
setPlotlyError(error.message || 'Unknown plotting error')
210-
}}
211-
/>
210+
<div className="w-full h-full min-h-[400px] flex flex-col">
211+
<div className="flex-1 min-h-0">
212+
<Plot
213+
data={plotData}
214+
layout={layout}
215+
config={config}
216+
style={{ width: '100%', height: '100%' }}
217+
useResizeHandler={true}
218+
onClick={handleClick}
219+
onSelected={handleSelected}
220+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
221+
onError={(error: any) => {
222+
console.error('Plotly error:', error)
223+
setPlotlyError(error.message || 'Unknown plotting error')
224+
}}
225+
/>
226+
</div>
227+
<div className="flex-shrink-0 px-4 py-1 text-sm text-muted-foreground border-t flex">
228+
<div>
229+
x: {xColumn} | y: {yColumn}
230+
{colorColumn && ` | color: ${colorDisplayName}`}
231+
{breakdownColumn && ` | breakdown: ${breakdownColumn}`}
232+
</div>
233+
<div className="flex-1" />
234+
<div>
235+
{bubbleStats.total} bubbles | size: {bubbleStats.minSize.toLocaleString()} to {bubbleStats.maxSize.toLocaleString()}
236+
</div>
237+
</div>
212238
</div>
213239
)
214240
})

smoosense-gui/src/lib/features/bubblePlot/bubblePlotSlice.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createAsyncDataSlice, type BaseAsyncDataState } from '@/lib/utils/creat
33
import { sanitizeName } from '@/lib/utils/sql/helpers'
44
import { isNil } from 'lodash'
55
import _ from 'lodash'
6+
import { BUBBLE_SIZE_COLOR_VALUE } from './BubblePlotMoreControls'
67

78
// BubblePlot data types
89
export interface BubblePlotDataPoint {
@@ -70,12 +71,16 @@ const fetchBubblePlotFunction = async (
7071
const tableRef = queryEngine === 'lance' ? 'lance_table' : `'${tablePath}'`
7172

7273
// Build query with optional color column
73-
const colorColumnSelect = !bubblePlotColorColumn
74+
// Handle special __bubble_size__ value - use COUNT(*) as color value
75+
const isBubbleSizeColor = bubblePlotColorColumn === BUBBLE_SIZE_COLOR_VALUE
76+
const colorColumnSelect = (!bubblePlotColorColumn || isBubbleSizeColor)
7477
? 'NULL AS color_col'
7578
: `${sanitizeName(bubblePlotColorColumn)} AS color_col`
7679
const colorColumnAgg = !bubblePlotColorColumn
7780
? 'NULL AS color_value'
78-
: 'AVG(color_col) AS color_value'
81+
: isBubbleSizeColor
82+
? 'COUNT(*) AS color_value'
83+
: 'AVG(color_col) AS color_value'
7984

8085
const query = `
8186
WITH filtered AS (

smoosense-gui/src/lib/features/ui/uiSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const initialState: UiState = {
8787
bubblePlotMinMarkerSize: 7,
8888
bubblePlotOpacity: 0.7,
8989
bubblePlotMarkerSizeContrastRatio: 4.2,
90-
bubblePlotColorColumn: '',
90+
bubblePlotColorColumn: '__bubble_size__',
9191
bubblePlotColorScale: 'Jet',
9292
heatmapXColumn: null,
9393
heatmapYColumn: null,

0 commit comments

Comments
 (0)