Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Optimize memory for plot tooltips #1876

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 76 additions & 21 deletions ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type Packed<T> = T | S
export interface Data {
list(): (Rec | null)[]
dict(): Dict<Rec>
getTupByIdx(i: U): Rec | null
}

interface OpsD {
Expand Down Expand Up @@ -399,6 +400,14 @@ export function unpack<T>(data: any): T {
: data
}

export function unpackByIdx<T = any>(data: any, idx: U): T {
return (typeof data === 'string')
? decodeStringByIdx(data, idx)
: (isData(data))
? data.getTupByIdx(idx)
: data?.[idx] || getObjectValueByIdx(data, idx)
}

const
errorCodes: Dict<WaveErrorCode> = {
not_found: WaveErrorCode.PageNotFound,
Expand All @@ -407,6 +416,22 @@ const
const i = d.indexOf(':')
return (i > 0) ? [d.substring(0, i), d.substring(i + 1)] : ['', d]
},
rowToRowObj = (item: any, fields: any) => {
const rec: Rec = {}
for (let j = 0; j < fields.length; j++) {
const f = fields[j], v = item[j]
rec[f] = v
}
return rec
},
colToRowObj = (columns: any, fields: any, idx: U) => {
const rec: Rec = {}
for (let j = 0; j < fields.length; j++) {
const f = fields[j], v = columns[j][idx]
rec[f] = v
}
return rec
},
decodeString = (data: S): any => {
if (data === '') return data
const [t, d] = decodeType(data)
Expand All @@ -423,17 +448,11 @@ const
const [fields, rows] = JSON.parse(d)
if (!Array.isArray(fields)) return data
if (!Array.isArray(rows)) return data
const w = fields.length // width
const recs: Rec[] = []
for (const r of rows) {
if (!Array.isArray(r)) continue
if (r.length !== w) continue
const rec: Rec = {}
for (let j = 0; j < w; j++) {
const f = fields[j], v = r[j]
rec[f] = v
}
recs.push(rec)
if (r.length !== fields.length) continue
recs.push(rowToRowObj(r, fields))
}
return recs
} catch (e) {
Expand All @@ -445,18 +464,12 @@ const
const [fields, columns] = JSON.parse(d)
if (!Array.isArray(fields)) return data
if (!Array.isArray(columns)) return data
const w = fields.length // width
if (columns.length !== w) return data
if (columns.length !== fields.length) return data
if (columns.length === 0) return data
const n = columns[0].length
const recs = new Array<Rec>(n)
const recs: Rec[] = []
for (let i = 0; i < n; i++) {
const rec: Rec = {}
for (let j = 0; j < w; j++) {
const f = fields[j], v = columns[j][i]
rec[f] = v
}
recs[i] = rec
recs.push(colToRowObj(columns, fields, i))
}
return recs
} catch (e) {
Expand All @@ -466,6 +479,31 @@ const
}
return data
},
decodeStringByIdx = (data: S, idx: U): any => {
if (data === '') return
const [t, d] = decodeType(data)
try {
const parsedData = JSON.parse(d)
const [fields, items] = parsedData
if (Array.isArray(fields) && Array.isArray(items)) {
if (t === 'rows') return rowToRowObj(items[idx], fields)
if (t === 'cols' && items.length && items.length === fields.length) return colToRowObj(items, fields, idx)
}
return parsedData?.[idx]
} catch (e) {
console.error(e)
}
},
getObjectValueByIdx = <T = any>(data: any, idx: U): T | undefined => {
// const sortedKeys = Object.keys(data).sort()
// return data?.[sortedKeys[idx]]
let i = 0
for (const k in data) {
if (i === idx) return data?.[k]
i++
}
return
},
keysOf = <T extends {}>(d: Dict<T>): S[] => {
const a: S[] = []
for (const k in d) a.push(k)
Expand All @@ -475,7 +513,6 @@ const
const a: T[] = []
for (const k in d) a.push(d[k])
return a

},
isMap = (x: any): B => {
// for JSON data only: anything not null, string, number, bool, array
Expand Down Expand Up @@ -566,13 +603,20 @@ const
}
return null
},
getTupByIdx = (i: U): Rec | null => {
if (i >= 0 && i < n) {
const tup = tups[i]
if (tup) return t.make(tup)
}
return null
},
list = (): (Rec | null)[] => {
const xs: (Rec | null)[] = []
for (const tup of tups) xs.push(tup ? t.make(tup) : null)
return xs
},
dict = (): Dict<Rec> => ({})
return { __buf__: true, n, put, set, seti, get, geti, list, dict }
return { __buf__: true, n, put, set, seti, get, geti, getTupByIdx, list, dict }
},
newCycBuf = (t: Typ, tups: (Tup | null)[], i: U): CycBuf => {
const
Expand All @@ -590,6 +634,13 @@ const
get = (_k: S): Cur | null => {
return b.geti(i)
},
getTupByIdx = (i: U): Rec | null => {
if (i >= 0 && i < n) {
const tup = tups[i]
if (tup) return t.make(tup)
}
return null
},
list = (): Rec[] => {
const xs: Rec[] = []
for (let j = i, k = 0; k < n; j++, k++) {
Expand All @@ -600,7 +651,7 @@ const
return xs
},
dict = (): Dict<Rec> => ({})
return { __buf__: true, put, set, get, list, dict }
return { __buf__: true, put, set, get, getTupByIdx, list, dict }
},
newMapBuf = (t: Typ, tups: Dict<Tup>): MapBuf => {
const
Expand All @@ -624,6 +675,10 @@ const
const tup = tups[k]
return tup ? newCur(t, tup) : null
},
getTupByIdx = (i: U): Rec | null => {
const tup = getObjectValueByIdx(tups, i)
return tup ? t.make(tup) : null
},
list = (): Rec[] => {
const keys = keysOf(tups)
keys.sort()
Expand All @@ -636,7 +691,7 @@ const
for (const k in tups) d[k] = t.make(tups[k])
return d
}
return { __buf__: true, put, set, get, list, dict }
return { __buf__: true, put, set, get, getTupByIdx, list, dict }
},
newTups = (n: U) => {
const xs = new Array<Tup | null>(n)
Expand Down
1 change: 1 addition & 0 deletions ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"build": "tsc",
"build-dev": "tsc --project dev.tsconfig.json",
"watch": "tsc --project dev.tsconfig.json --watch",
"minify": "terser dist/index.js -c -m -o dist/index.min.js",
"prepublishOnly": "npm run build && npm run minify",
"test": "echo \"Error: no test specified\" && exit 1"
Expand Down
24 changes: 10 additions & 14 deletions ui/src/plot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { Chart } from '@antv/g2'
import { AdjustOption, AnnotationPosition, ArcOption, AxisOption, ChartCfg, CoordinateActions, CoordinateOption, DataMarkerOption, DataRegionOption, GeometryOption, LineOption, RegionOption, ScaleOption, TextOption, TooltipItem } from '@antv/g2/lib/interface'
import { B, Dict, Disposable, F, Model, on, parseI, parseU, Rec, S, unpack, V } from 'h2o-wave'
import { B, Dict, Disposable, F, Model, on, parseI, parseU, Rec, S, unpack, unpackByIdx, V } from 'h2o-wave'
import React from 'react'
import ReactDOM from 'react-dom'
import { stylesheet } from 'typestyle'
Expand Down Expand Up @@ -1040,19 +1040,18 @@ export interface Visualization {
const tooltipContainer = document.createElement('div')
tooltipContainer.className = 'g2-tooltip'

const PlotTooltip = ({ items, originalItems }: { items: TooltipItem[], originalItems: any[] }) =>
const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalData: Rec }) =>
<>
{items.map(({ data, mappingData, color }: TooltipItem) =>
Object.keys(originalItems[data.idx]).map((itemKey, idx) => {
const item = originalItems[data.idx][itemKey]
return <li key={idx} className="g2-tooltip-list-item" data-index={idx} style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
Object.entries(unpackByIdx<any>(originalData, data.idx)).map(([key, item], idx) =>
<li key={idx} className="g2-tooltip-list-item" data-index={idx} style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
<span style={{ backgroundColor: mappingData?.color || color }} className="g2-tooltip-marker" />
<span style={{ display: 'inline-flex', flex: 1, justifyContent: 'space-between' }}>
<span style={{ marginRight: 16 }}>{itemKey}:</span>
<span style={{ marginRight: 16 }}>{key}:</span>
<span>{(item instanceof Date ? item.toISOString().split('T')[0] : item)}</span>
</span>
</li>
}

)
)}
</>
Expand All @@ -1066,7 +1065,6 @@ export const
currentChart = React.useRef<Chart | null>(null),
currentPlot = React.useRef<Plot | null>(null),
themeWatchRef = React.useRef<Disposable | null>(null),
originalDataRef = React.useRef<any[]>([]),
checkDimensionsPostInit = (w: F, h: F) => { // Safari fix
const el = container.current
if (!el) return
Expand All @@ -1091,7 +1089,6 @@ export const
data = refactorData(raw_data, plot.marks),
{ Chart } = await import('@antv/g2'),
chart = plot.marks ? new Chart(makeChart(el, space, plot.marks, model.interactions || [])) : null
originalDataRef.current = unpack<any[]>(model.data)
currentPlot.current = plot
if (chart) {
chart.tooltip({
Expand All @@ -1104,10 +1101,10 @@ export const
color: cssVar('$text')
},
},
customContent: (_title, items) => {
ReactDOM.render(<PlotTooltip items={items} originalItems={originalDataRef.current} />, tooltipContainer)
return tooltipContainer
}
customContent: () => tooltipContainer
})
chart.on('tooltip:change', ({ data }: any) => {
ReactDOM.render(<PlotTooltip items={data.items} originalData={model.data} />, tooltipContainer)
})
currentChart.current = chart
chart.data(data)
Expand Down Expand Up @@ -1155,7 +1152,6 @@ export const
const
raw_data = unpack<any[]>(model.data),
data = refactorData(raw_data, currentPlot.current.marks)
originalDataRef.current = unpack<any[]>(model.data)
currentChart.current.changeData(data)
}, [currentChart, currentPlot, model])

Expand Down
3 changes: 3 additions & 0 deletions ui/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export default defineConfig({
assetsDir: 'wave-static',
chunkSizeWarningLimit: 900
},
optimizeDeps: {
link: ['h2o-wave']
},
server: {
port: 3000,
proxy: {
Expand Down