From 9d19c14fd91124f4836518370e6299314e327bf6 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Wed, 1 Mar 2023 10:28:25 +0100 Subject: [PATCH 01/14] chore: add unpackByIdx fn - broken version --- py/examples/plot_issue_buffers.py | 52 +++++++++++++++++++++++++++++++ ts/index.ts | 41 ++++++++++++++++++------ ui/src/plot.tsx | 29 ++++++++++++----- ui/vite.config.js | 4 +++ 4 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 py/examples/plot_issue_buffers.py diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py new file mode 100644 index 0000000000..d32ce918ef --- /dev/null +++ b/py/examples/plot_issue_buffers.py @@ -0,0 +1,52 @@ +from h2o_wave import main, app, Q, ui, data + + +# Array buffer +a = data(fields='price low high', size=8, rows=[ + [4, 50, 100], + [6, 100, 150], + [8, 150, 200], + [16, 350, 400], + [18, 400, 450], + [10, 200, 250], + [12, 250, 300], + [14, 300, 350], + ], + pack=True + ) + +# Cyclic buffer +c = data(fields='price low high', size=-8, rows=[ + [4, 50, 100], + [6, 100, 150], + [8, 150, 200], + [16, 350, 400], + [18, 400, 450], + [10, 200, 250], + [12, 250, 300], + [14, 300, 350], + ]) + +# Map buffer +m = data(fields='price low high', rows=dict( + fst=[4, 50, 100], + snd=[6, 100, 150], + trd=[8, 150, 200], + fth=[16, 350, 400], + fih=[18, 400, 450], + sth=[10, 200, 250], + seh=[12, 250, 300], + nth=[14, 300, 350], +)) + +@app('/demo') +async def serve(q: Q): + q.page['meta'] = ui.meta_card(box='') + q.page['example'] = ui.plot_card( + box='1 1 4 5', + title='Histogram', + data=a, + plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), + ) + + await q.page.save() diff --git a/ts/index.ts b/ts/index.ts index b790fdf367..b51b24098c 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -182,6 +182,7 @@ export type Packed = T | S export interface Data { list(): (Rec | null)[] dict(): Dict + getTupByIdx(i: U): Rec | null } interface OpsD { @@ -399,6 +400,14 @@ export function unpack(data: any): T { : data } +export function unpackByIdx(data: any, idx: U): T { + return (typeof data === 'string') + ? decodeString(data, idx) // TODO: packed data + : (isData(data)) + ? (data as Data).getTupByIdx(idx) + : data +} + const errorCodes: Dict = { not_found: WaveErrorCode.PageNotFound, @@ -407,27 +416,29 @@ const const i = d.indexOf(':') return (i > 0) ? [d.substring(0, i), d.substring(i + 1)] : ['', d] }, - decodeString = (data: S): any => { + // 'rows:[["price","low","high"],[[4,50,100],[6,100,150],[8,150,200],[16,350,400],[18,400,450],[10,200,250],[12,250,300],[14,300,350]]]' // TODO: + decodeString = (data: S, idx?: U): any => { if (data === '') return data const [t, d] = decodeType(data) switch (t) { case 'data': try { - return JSON.parse(d) + return JSON.parse(d) // TODO: } catch (e) { console.error(e) } break case 'rows': try { - const [fields, rows] = JSON.parse(d) + const [fields, items] = JSON.parse(d) if (!Array.isArray(fields)) return data - if (!Array.isArray(rows)) return data + if (!Array.isArray(items)) return data const w = fields.length // width const recs: Rec[] = [] + const rows = (idx === undefined) ? items : [items[idx]] for (const r of rows) { if (!Array.isArray(r)) continue - if (r.length !== w) continue + if (r.length !== w) continue // TODO: const rec: Rec = {} for (let j = 0; j < w; j++) { const f = fields[j], v = r[j] @@ -448,7 +459,7 @@ const const w = fields.length // width if (columns.length !== w) return data if (columns.length === 0) return data - const n = columns[0].length + const n = columns[0].length // TODO: const recs = new Array(n) for (let i = 0; i < n; i++) { const rec: Rec = {} @@ -566,13 +577,17 @@ const } return null }, + getTupByIdx = (i: U): (Rec | null) => { + if (i >= 0 && i < n && tups && tups[i]) return t.make(tups[i] as any[]) // TODO: + 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 => ({}) - 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 @@ -590,6 +605,10 @@ const get = (_k: S): Cur | null => { return b.geti(i) }, + getTupByIdx = (i: U): (Rec | null) => { + if (i >= 0 && i < n && tups && tups[i]) return t.make(tups[i] as any[]) // TODO: + return null + }, list = (): Rec[] => { const xs: Rec[] = [] for (let j = i, k = 0; k < n; j++, k++) { @@ -600,7 +619,7 @@ const return xs }, dict = (): Dict => ({}) - return { __buf__: true, put, set, get, list, dict } + return { __buf__: true, put, set, get, getTupByIdx, list, dict } }, newMapBuf = (t: Typ, tups: Dict): MapBuf => { const @@ -624,6 +643,10 @@ const const tup = tups[k] return tup ? newCur(t, tup) : null }, + getTupByIdx = (i: U): Rec | null => { + const k = keysOf(tups)[i] // TODO: ?? + return t.make(tups[k]) // TODO: + }, list = (): Rec[] => { const keys = keysOf(tups) keys.sort() @@ -636,7 +659,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(n) diff --git a/ui/src/plot.tsx b/ui/src/plot.tsx index cce444f866..d608f4fe58 100644 --- a/ui/src/plot.tsx +++ b/ui/src/plot.tsx @@ -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' @@ -1040,11 +1040,24 @@ 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: any[] }) => <> - {items.map(({ data, mappingData, color }: TooltipItem) => - Object.keys(originalItems[data.idx]).map((itemKey, idx) => { - const item = originalItems[data.idx][itemKey] + {items.map(({ data, mappingData, color }: TooltipItem) => { + const originalItems = unpackByIdx(originalData, data.idx) + // const originalItems = originalData[data.idx] + // const originalItems = originalData.length + // ? originalData[data.idx] + // : (originalData as Rec).list()[data.idx] + + // : Object.keys(data).reduce((acc, key) => { + // if (key !== 'idx') { + // const value = (originalData as Rec).get(data.idx).get(key) + // if (value) return ({ ...acc, [key]: value }) + // } + // return acc + // }, {}) + return Object.keys(originalItems).map((itemKey, idx) => { + const item = originalItems[itemKey] return
  • @@ -1054,6 +1067,7 @@ const PlotTooltip = ({ items, originalItems }: { items: TooltipItem[], originalI
  • } ) + } )} @@ -1067,6 +1081,7 @@ export const currentPlot = React.useRef(null), themeWatchRef = React.useRef(null), originalDataRef = React.useRef([]), + // originalDataRef = React.useRef(unpackByIdx(model.data)), checkDimensionsPostInit = (w: F, h: F) => { // Safari fix const el = container.current if (!el) return @@ -1105,7 +1120,7 @@ export const }, }, customContent: (_title, items) => { - ReactDOM.render(, tooltipContainer) + ReactDOM.render(, tooltipContainer) return tooltipContainer } }) @@ -1155,7 +1170,7 @@ export const const raw_data = unpack(model.data), data = refactorData(raw_data, currentPlot.current.marks) - originalDataRef.current = unpack(model.data) + // originalDataRef.current = unpackByIdx(model.data) currentChart.current.changeData(data) }, [currentChart, currentPlot, model]) diff --git a/ui/vite.config.js b/ui/vite.config.js index 7882c4605c..ad11810410 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -31,6 +31,10 @@ export default defineConfig({ assetsDir: 'wave-static', chunkSizeWarningLimit: 900 }, + optimizeDeps: { + // Force pre-bundling of linked package + include: ['h2o-wave'], + }, server: { port: 3000, proxy: { From 0f9146b6c48c453118db7dcd9702e2d48e00fa56 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Fri, 3 Mar 2023 13:13:05 +0100 Subject: [PATCH 02/14] chore: live reload linked dependencies --- ts/package.json | 2 +- ui/vite.config.js | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ts/package.json b/ts/package.json index 0a3c073d8b..c40b46ce34 100644 --- a/ts/package.json +++ b/ts/package.json @@ -6,7 +6,7 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "build-dev": "tsc --project dev.tsconfig.json", + "build-dev": "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" diff --git a/ui/vite.config.js b/ui/vite.config.js index ad11810410..afe0343713 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -16,6 +16,7 @@ import { defineConfig } from 'vite' import legacy from '@vitejs/plugin-legacy' import eslintPlugin from 'vite-plugin-eslint' import react from '@vitejs/plugin-react' +import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ @@ -29,11 +30,14 @@ export default defineConfig({ build: { outDir: 'build', assetsDir: 'wave-static', - chunkSizeWarningLimit: 900 + chunkSizeWarningLimit: 900, }, optimizeDeps: { - // Force pre-bundling of linked package - include: ['h2o-wave'], + link: ['h2o-wave'], + }, + alias: { + 'h2o-wave': '/@linked/h2o-wave/index.ts', + '/@linked/h2o-wave/': path.resolve(require.resolve('h2o-wave/package.json'), '../') }, server: { port: 3000, From 668ade7d52881fa679f55c4580a0471f1287878d Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Mon, 6 Mar 2023 10:34:11 +0100 Subject: [PATCH 03/14] chore: add watch mode option for build ts into package.json --- ts/package.json | 3 ++- ui/vite.config.js | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ts/package.json b/ts/package.json index c40b46ce34..414553d407 100644 --- a/ts/package.json +++ b/ts/package.json @@ -6,7 +6,8 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "build-dev": "tsc --project dev.tsconfig.json --watch", + "build-dev": "tsc --project dev.tsconfig.json", + "build-dev-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" diff --git a/ui/vite.config.js b/ui/vite.config.js index afe0343713..09a34e3ff4 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -35,10 +35,6 @@ export default defineConfig({ optimizeDeps: { link: ['h2o-wave'], }, - alias: { - 'h2o-wave': '/@linked/h2o-wave/index.ts', - '/@linked/h2o-wave/': path.resolve(require.resolve('h2o-wave/package.json'), '../') - }, server: { port: 3000, proxy: { From d61e8bb10c84883fff899a83f88906d7f866c190 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Mon, 6 Mar 2023 17:59:29 +0100 Subject: [PATCH 04/14] chore: add tooltip support for packed data, add support for columns --- py/examples/plot_issue_buffers.py | 6 +++- ts/index.ts | 50 +++++++++++++++++-------------- ui/src/plot.tsx | 12 -------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py index d32ce918ef..a94e2e7fa7 100644 --- a/py/examples/plot_issue_buffers.py +++ b/py/examples/plot_issue_buffers.py @@ -15,6 +15,10 @@ pack=True ) +ac = data(fields='price low high', size=8, columns=[[4,6,8,16,18,10,12,14],[50,100,150,350,400,200,250,300],[100,150,200,400,450,250,300,350]], + pack=True + ) + # Cyclic buffer c = data(fields='price low high', size=-8, rows=[ [4, 50, 100], @@ -45,7 +49,7 @@ async def serve(q: Q): q.page['example'] = ui.plot_card( box='1 1 4 5', title='Histogram', - data=a, + data=ac, plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), ) diff --git a/ts/index.ts b/ts/index.ts index b51b24098c..963cbc82af 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -402,7 +402,7 @@ export function unpack(data: any): T { export function unpackByIdx(data: any, idx: U): T { return (typeof data === 'string') - ? decodeString(data, idx) // TODO: packed data + ? decodeString(data, idx) : (isData(data)) ? (data as Data).getTupByIdx(idx) : data @@ -416,7 +416,22 @@ const const i = d.indexOf(':') return (i > 0) ? [d.substring(0, i), d.substring(i + 1)] : ['', d] }, - // 'rows:[["price","low","high"],[[4,50,100],[6,100,150],[8,150,200],[16,350,400],[18,400,450],[10,200,250],[12,250,300],[14,300,350]]]' // TODO: + 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, idx?: U): any => { if (data === '') return data const [t, d] = decodeType(data) @@ -430,21 +445,15 @@ const break case 'rows': try { - const [fields, items] = JSON.parse(d) + const [fields, rows] = JSON.parse(d) if (!Array.isArray(fields)) return data - if (!Array.isArray(items)) return data - const w = fields.length // width + if (!Array.isArray(rows)) return data + if (idx !== undefined) return rowToRowObj(rows[idx], fields) const recs: Rec[] = [] - const rows = (idx === undefined) ? items : [items[idx]] for (const r of rows) { if (!Array.isArray(r)) continue - if (r.length !== w) continue // TODO: - 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) { @@ -456,18 +465,13 @@ 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 // TODO: - const recs = new Array(n) + if (idx !== undefined) return colToRowObj(columns, fields, idx) + const n = columns[0].length + 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) { diff --git a/ui/src/plot.tsx b/ui/src/plot.tsx index d608f4fe58..99f8762641 100644 --- a/ui/src/plot.tsx +++ b/ui/src/plot.tsx @@ -1044,18 +1044,6 @@ const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalDa <> {items.map(({ data, mappingData, color }: TooltipItem) => { const originalItems = unpackByIdx(originalData, data.idx) - // const originalItems = originalData[data.idx] - // const originalItems = originalData.length - // ? originalData[data.idx] - // : (originalData as Rec).list()[data.idx] - - // : Object.keys(data).reduce((acc, key) => { - // if (key !== 'idx') { - // const value = (originalData as Rec).get(data.idx).get(key) - // if (value) return ({ ...acc, [key]: value }) - // } - // return acc - // }, {}) return Object.keys(originalItems).map((itemKey, idx) => { const item = originalItems[itemKey] return
  • From 93498b3732af5ece26526a24ea3e6e4002074072 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Tue, 7 Mar 2023 11:07:39 +0100 Subject: [PATCH 05/14] chore: refactor buffer getTupByIdx functions --- py/examples/plot_issue_buffers.py | 36 +++++++++++++++++++------------ ts/index.ts | 22 +++++++++++++------ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py index a94e2e7fa7..dcf659bbac 100644 --- a/py/examples/plot_issue_buffers.py +++ b/py/examples/plot_issue_buffers.py @@ -1,8 +1,8 @@ from h2o_wave import main, app, Q, ui, data -# Array buffer -a = data(fields='price low high', size=8, rows=[ +# Array buffer - rows +ar = data(fields='price low high', size=8, rows=[ [4, 50, 100], [6, 100, 150], [8, 150, 200], @@ -11,16 +11,17 @@ [10, 200, 250], [12, 250, 300], [14, 300, 350], - ], - pack=True - ) + ], pack=False) -ac = data(fields='price low high', size=8, columns=[[4,6,8,16,18,10,12,14],[50,100,150,350,400,200,250,300],[100,150,200,400,450,250,300,350]], - pack=True - ) +# Array buffer - columns +ac = data(fields='price low high', size=8, columns=[ + [4,6,8,16,18,10,12,14], + [50,100,150,350,400,200,250,300], + [100,150,200,400,450,250,300,350] + ], pack=False) -# Cyclic buffer -c = data(fields='price low high', size=-8, rows=[ +# Cyclic buffer - rows +cr = data(fields='price low high', size=-8, rows=[ [4, 50, 100], [6, 100, 150], [8, 150, 200], @@ -29,10 +30,10 @@ [10, 200, 250], [12, 250, 300], [14, 300, 350], - ]) + ], pack=False) # pack=False not working -# Map buffer -m = data(fields='price low high', rows=dict( +# Map buffer - rows +mr = data(fields='price low high', rows=dict( fst=[4, 50, 100], snd=[6, 100, 150], trd=[8, 150, 200], @@ -43,13 +44,20 @@ nth=[14, 300, 350], )) +# Map buffer - columns +# mc = data(fields='price low high', columns=dict( +# fst=[4,6,8,16,18,10,12,14], +# snd=[50,100,150,350,400,200,250,300], +# trd=[100,150,200,400,450,250,300,350] +# )) + @app('/demo') async def serve(q: Q): q.page['meta'] = ui.meta_card(box='') q.page['example'] = ui.plot_card( box='1 1 4 5', title='Histogram', - data=ac, + data=ar, plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), ) diff --git a/ts/index.ts b/ts/index.ts index 963cbc82af..3c09eb15c6 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -438,7 +438,9 @@ const switch (t) { case 'data': try { - return JSON.parse(d) // TODO: + const parsedData = JSON.parse(d) + if (idx !== undefined && parsedData?.[idx]) return parsedData[idx] + return parsedData } catch (e) { console.error(e) } @@ -581,8 +583,11 @@ const } return null }, - getTupByIdx = (i: U): (Rec | null) => { - if (i >= 0 && i < n && tups && tups[i]) return t.make(tups[i] as any[]) // TODO: + 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)[] => { @@ -609,8 +614,11 @@ const get = (_k: S): Cur | null => { return b.geti(i) }, - getTupByIdx = (i: U): (Rec | null) => { - if (i >= 0 && i < n && tups && tups[i]) return t.make(tups[i] as any[]) // TODO: + getTupByIdx = (i: U): Rec | null => { + if (i >= 0 && i < n) { + const tup = tups[i] + if (tup) return t.make(tup) + } return null }, list = (): Rec[] => { @@ -648,8 +656,8 @@ const return tup ? newCur(t, tup) : null }, getTupByIdx = (i: U): Rec | null => { - const k = keysOf(tups)[i] // TODO: ?? - return t.make(tups[k]) // TODO: + const k = keysOf(tups)[i] + return t.make(tups[k]) }, list = (): Rec[] => { const keys = keysOf(tups) From aadd3f9241c476ab2f005f06a47cd9e305c74b59 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Tue, 7 Mar 2023 13:53:56 +0100 Subject: [PATCH 06/14] chore: remove redundant refs --- ui/src/plot.tsx | 8 ++------ ui/vite.config.js | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ui/src/plot.tsx b/ui/src/plot.tsx index 99f8762641..a496460e84 100644 --- a/ui/src/plot.tsx +++ b/ui/src/plot.tsx @@ -1040,7 +1040,7 @@ export interface Visualization { const tooltipContainer = document.createElement('div') tooltipContainer.className = 'g2-tooltip' -const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalData: any[] }) => +const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalData: Rec }) => <> {items.map(({ data, mappingData, color }: TooltipItem) => { const originalItems = unpackByIdx(originalData, data.idx) @@ -1068,8 +1068,6 @@ export const currentChart = React.useRef(null), currentPlot = React.useRef(null), themeWatchRef = React.useRef(null), - originalDataRef = React.useRef([]), - // originalDataRef = React.useRef(unpackByIdx(model.data)), checkDimensionsPostInit = (w: F, h: F) => { // Safari fix const el = container.current if (!el) return @@ -1094,7 +1092,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(model.data) currentPlot.current = plot if (chart) { chart.tooltip({ @@ -1108,7 +1105,7 @@ export const }, }, customContent: (_title, items) => { - ReactDOM.render(, tooltipContainer) + ReactDOM.render(, tooltipContainer) return tooltipContainer } }) @@ -1158,7 +1155,6 @@ export const const raw_data = unpack(model.data), data = refactorData(raw_data, currentPlot.current.marks) - // originalDataRef.current = unpackByIdx(model.data) currentChart.current.changeData(data) }, [currentChart, currentPlot, model]) diff --git a/ui/vite.config.js b/ui/vite.config.js index 09a34e3ff4..ce83683f40 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -30,10 +30,10 @@ export default defineConfig({ build: { outDir: 'build', assetsDir: 'wave-static', - chunkSizeWarningLimit: 900, + chunkSizeWarningLimit: 900 }, optimizeDeps: { - link: ['h2o-wave'], + link: ['h2o-wave'] }, server: { port: 3000, From 3537ba62b3cb70cff9be479844fcdc57e7998d52 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Tue, 7 Mar 2023 13:54:38 +0100 Subject: [PATCH 07/14] chore: update example to make sure the new implementation counts with dynamic data changes --- py/examples/plot_issue_buffers.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py index dcf659bbac..776dcf32e0 100644 --- a/py/examples/plot_issue_buffers.py +++ b/py/examples/plot_issue_buffers.py @@ -53,12 +53,17 @@ @app('/demo') async def serve(q: Q): - q.page['meta'] = ui.meta_card(box='') - q.page['example'] = ui.plot_card( - box='1 1 4 5', - title='Histogram', - data=ar, - plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), - ) + if not q.client.initialized: + q.page['meta'] = ui.meta_card(box='') + q.page['example'] = ui.plot_card( + box='1 1 4 5', + title='Histogram', + data=ar, + plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), + ) + q.page['btn'] = ui.form_card(box='5 6 2 2', items=[ui.button(name='change_data', label='Change data', primary=True)]) + q.client.initialized = True + elif q.args.change_data: + q.page['example'].data[2] = [9,160,210] await q.page.save() From 498f6d07c23bd0c764d6919f95b0231ae6df03f7 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Tue, 7 Mar 2023 13:55:10 +0100 Subject: [PATCH 08/14] chore: delete test example --- py/examples/plot_issue_buffers.py | 69 ------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 py/examples/plot_issue_buffers.py diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py deleted file mode 100644 index 776dcf32e0..0000000000 --- a/py/examples/plot_issue_buffers.py +++ /dev/null @@ -1,69 +0,0 @@ -from h2o_wave import main, app, Q, ui, data - - -# Array buffer - rows -ar = data(fields='price low high', size=8, rows=[ - [4, 50, 100], - [6, 100, 150], - [8, 150, 200], - [16, 350, 400], - [18, 400, 450], - [10, 200, 250], - [12, 250, 300], - [14, 300, 350], - ], pack=False) - -# Array buffer - columns -ac = data(fields='price low high', size=8, columns=[ - [4,6,8,16,18,10,12,14], - [50,100,150,350,400,200,250,300], - [100,150,200,400,450,250,300,350] - ], pack=False) - -# Cyclic buffer - rows -cr = data(fields='price low high', size=-8, rows=[ - [4, 50, 100], - [6, 100, 150], - [8, 150, 200], - [16, 350, 400], - [18, 400, 450], - [10, 200, 250], - [12, 250, 300], - [14, 300, 350], - ], pack=False) # pack=False not working - -# Map buffer - rows -mr = data(fields='price low high', rows=dict( - fst=[4, 50, 100], - snd=[6, 100, 150], - trd=[8, 150, 200], - fth=[16, 350, 400], - fih=[18, 400, 450], - sth=[10, 200, 250], - seh=[12, 250, 300], - nth=[14, 300, 350], -)) - -# Map buffer - columns -# mc = data(fields='price low high', columns=dict( -# fst=[4,6,8,16,18,10,12,14], -# snd=[50,100,150,350,400,200,250,300], -# trd=[100,150,200,400,450,250,300,350] -# )) - -@app('/demo') -async def serve(q: Q): - if not q.client.initialized: - q.page['meta'] = ui.meta_card(box='') - q.page['example'] = ui.plot_card( - box='1 1 4 5', - title='Histogram', - data=ar, - plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), - ) - q.page['btn'] = ui.form_card(box='5 6 2 2', items=[ui.button(name='change_data', label='Change data', primary=True)]) - q.client.initialized = True - elif q.args.change_data: - q.page['example'].data[2] = [9,160,210] - - await q.page.save() From 591d3da9f89c9ee8ae585158906d6b5f9d45e506 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Thu, 9 Mar 2023 12:12:32 +0100 Subject: [PATCH 09/14] chore: refactor, compute custom tooltip content only on tooltip change --- py/examples/plot_issue_buffers.py | 69 +++++++++++++++++++++++++++++++ ts/index.ts | 53 +++++++++++++++++++----- ts/package.json | 2 +- ui/src/plot.tsx | 18 ++++---- ui/vite.config.js | 1 - 5 files changed, 121 insertions(+), 22 deletions(-) create mode 100644 py/examples/plot_issue_buffers.py diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py new file mode 100644 index 0000000000..c7d5626551 --- /dev/null +++ b/py/examples/plot_issue_buffers.py @@ -0,0 +1,69 @@ +from h2o_wave import main, app, Q, ui, data + + +# Array buffer - rows +ar = data(fields='price low high', size=8, rows=[ + [4, 50, 100], + [6, 100, 150], + [8, 150, 200], + [16, 350, 400], + [18, 400, 450], + [10, 200, 250], + [12, 250, 300], + [14, 300, 350], + ], pack=False) + +# Array buffer - columns +ac = data(fields='price low high', size=8, columns=[ + [4,6,8,16,18,10,12,14], + [50,100,150,350,400,200,250,300], + [100,150,200,400,450,250,300,350] + ], pack=False) + +# Cyclic buffer - rows +cr = data(fields='price low high', size=-8, rows=[ + [4, 50, 100], + [6, 100, 150], + [8, 150, 200], + [16, 350, 400], + [18, 400, 450], + [10, 200, 250], + [12, 250, 300], + [14, 300, 350], + ], pack=False) # pack=False not working + +# Map buffer - rows +mr = data(fields='price low high', rows=dict( + fst=[4, 50, 100], + snd=[6, 100, 150], + trd=[8, 150, 200], + fth=[16, 350, 400], + fih=[18, 400, 450], + sth=[10, 200, 250], + seh=[12, 250, 300], + nth=[14, 300, 350], +)) + +# Map buffer - columns +# mc = data(fields='price low high', columns=dict( +# fst=[4,6,8,16,18,10,12,14], +# snd=[50,100,150,350,400,200,250,300], +# trd=[100,150,200,400,450,250,300,350] +# )) + +@app('/demo') +async def serve(q: Q): + if not q.client.initialized: + q.page['meta'] = ui.meta_card(box='') + q.page['example'] = ui.plot_card( + box='1 1 4 5', + title='Histogram', + data=ar, + plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), + ) + q.page['btn'] = ui.form_card(box='5 6 2 2', items=[ui.button(name='change_data', label='Change data', primary=True)]) + q.client.initialized = True + elif q.args.change_data: + q.page['example'].data[2] = [9,160,210] + + await q.page.save() \ No newline at end of file diff --git a/ts/index.ts b/ts/index.ts index 3c09eb15c6..4cb9cb470a 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -400,12 +400,12 @@ export function unpack(data: any): T { : data } -export function unpackByIdx(data: any, idx: U): T { +export function unpackByIdx(data: any, idx: U): T { return (typeof data === 'string') - ? decodeString(data, idx) + ? decodeStringByIdx(data, idx) : (isData(data)) - ? (data as Data).getTupByIdx(idx) - : data + ? data.getTupByIdx(idx) + : data // TODO: } const @@ -432,15 +432,13 @@ const } return rec }, - decodeString = (data: S, idx?: U): any => { + decodeString = (data: S): any => { if (data === '') return data const [t, d] = decodeType(data) switch (t) { case 'data': try { - const parsedData = JSON.parse(d) - if (idx !== undefined && parsedData?.[idx]) return parsedData[idx] - return parsedData + return JSON.parse(d) } catch (e) { console.error(e) } @@ -450,7 +448,6 @@ const const [fields, rows] = JSON.parse(d) if (!Array.isArray(fields)) return data if (!Array.isArray(rows)) return data - if (idx !== undefined) return rowToRowObj(rows[idx], fields) const recs: Rec[] = [] for (const r of rows) { if (!Array.isArray(r)) continue @@ -469,7 +466,6 @@ const if (!Array.isArray(columns)) return data if (columns.length !== fields.length) return data if (columns.length === 0) return data - if (idx !== undefined) return colToRowObj(columns, fields, idx) const n = columns[0].length const recs: Rec[] = [] for (let i = 0; i < n; i++) { @@ -483,6 +479,43 @@ const } return data }, + decodeStringByIdx = (data: S, idx: U): any => { + if (data === '') return data + const [t, d] = decodeType(data) + switch (t) { + case 'data': + try { + const parsedData = JSON.parse(d) + return parsedData[idx] + } catch (e) { + console.error(e) + } + break + case 'rows': + try { + const [fields, rows] = JSON.parse(d) + if (!Array.isArray(fields)) return data // TODO: + if (!Array.isArray(rows)) return data + return rowToRowObj(rows[idx], fields) + } catch (e) { + console.error(e) + } + break + case 'cols': + try { + const [fields, columns] = JSON.parse(d) + if (!Array.isArray(fields)) return data + if (!Array.isArray(columns)) return data + if (columns.length !== fields.length) return data + if (columns.length === 0) return data + return colToRowObj(columns, fields, idx) + } catch (e) { + console.error(e) + } + break + } + return data + }, keysOf = (d: Dict): S[] => { const a: S[] = [] for (const k in d) a.push(k) diff --git a/ts/package.json b/ts/package.json index 414553d407..be82f669c9 100644 --- a/ts/package.json +++ b/ts/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "build-dev": "tsc --project dev.tsconfig.json", - "build-dev-watch": "tsc --project dev.tsconfig.json --watch", + "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" diff --git a/ui/src/plot.tsx b/ui/src/plot.tsx index a496460e84..96f817b6db 100644 --- a/ui/src/plot.tsx +++ b/ui/src/plot.tsx @@ -1043,17 +1043,15 @@ tooltipContainer.className = 'g2-tooltip' const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalData: Rec }) => <> {items.map(({ data, mappingData, color }: TooltipItem) => { - const originalItems = unpackByIdx(originalData, data.idx) - return Object.keys(originalItems).map((itemKey, idx) => { - const item = originalItems[itemKey] - return
  • + return Object.entries(unpackByIdx(originalData, data.idx)).map(([key, item], idx) => +
  • - {itemKey}: + {key}: {(item instanceof Date ? item.toISOString().split('T')[0] : item)}
  • - } + ) } )} @@ -1104,10 +1102,10 @@ export const color: cssVar('$text') }, }, - customContent: (_title, items) => { - ReactDOM.render(, tooltipContainer) - return tooltipContainer - } + customContent: () => tooltipContainer + }) + chart.on('tooltip:change', ({ data }: any) => { + ReactDOM.render(, tooltipContainer) }) currentChart.current = chart chart.data(data) diff --git a/ui/vite.config.js b/ui/vite.config.js index ce83683f40..53f1e265b3 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -16,7 +16,6 @@ import { defineConfig } from 'vite' import legacy from '@vitejs/plugin-legacy' import eslintPlugin from 'vite-plugin-eslint' import react from '@vitejs/plugin-react' -import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ From 74d987a63c9c05e57ee4ac927b64180417a81dff Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Thu, 9 Mar 2023 20:37:52 +0100 Subject: [PATCH 10/14] chore: unpack by idx for non-data objects --- ts/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ts/index.ts b/ts/index.ts index 4cb9cb470a..3386bae6a5 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -405,7 +405,8 @@ export function unpackByIdx(data: any, idx: U): T { ? decodeStringByIdx(data, idx) : (isData(data)) ? data.getTupByIdx(idx) - : data // TODO: + // TODO: Optimize - use for in. Object.entries() not supported in this ES version. + : data?.[idx] || data[Object.keys(data)?.[idx]] } const @@ -689,8 +690,12 @@ const return tup ? newCur(t, tup) : null }, getTupByIdx = (i: U): Rec | null => { - const k = keysOf(tups)[i] - return t.make(tups[k]) + let idx = 0 + for (const k in tups) { + if (idx === i && tups.hasOwnProperty(k)) return t.make(tups[k]) + idx++ + } + return null }, list = (): Rec[] => { const keys = keysOf(tups) From d42dc0cd1cd4dd3bec98d2c2cd0142ff7408127e Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Mon, 13 Mar 2023 13:39:09 +0100 Subject: [PATCH 11/14] chore: refactor decodeStringByIdx --- ts/index.ts | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/ts/index.ts b/ts/index.ts index 3386bae6a5..2297122bc5 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -393,6 +393,7 @@ export const } export function unpack(data: any): T { + console.log(data) return (typeof data === 'string') ? decodeString(data) : (isData(data)) @@ -483,39 +484,17 @@ const decodeStringByIdx = (data: S, idx: U): any => { if (data === '') return data const [t, d] = decodeType(data) - switch (t) { - case 'data': - try { - const parsedData = JSON.parse(d) - return parsedData[idx] - } catch (e) { - console.error(e) - } - break - case 'rows': - try { - const [fields, rows] = JSON.parse(d) - if (!Array.isArray(fields)) return data // TODO: - if (!Array.isArray(rows)) return data - return rowToRowObj(rows[idx], fields) - } catch (e) { - console.error(e) - } - break - case 'cols': - try { - const [fields, columns] = JSON.parse(d) - if (!Array.isArray(fields)) return data - if (!Array.isArray(columns)) return data - if (columns.length !== fields.length) return data - if (columns.length === 0) return data - return colToRowObj(columns, fields, idx) - } catch (e) { - console.error(e) - } - break + try { + if (t === 'data') return JSON.parse(d)[idx] + const [fields, items] = JSON.parse(d) + 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) + } + } catch (e) { + console.error(e) } - return data + return JSON.parse(d)[idx] }, keysOf = (d: Dict): S[] => { const a: S[] = [] From 199b9b8cc2fc660153958e593723609c6a3de59c Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Mon, 13 Mar 2023 14:25:20 +0100 Subject: [PATCH 12/14] chore: refactor getting the object value by idx --- py/examples/plot_issue_buffers.py | 69 ------------------------------- ts/index.ts | 25 +++++------ 2 files changed, 13 insertions(+), 81 deletions(-) delete mode 100644 py/examples/plot_issue_buffers.py diff --git a/py/examples/plot_issue_buffers.py b/py/examples/plot_issue_buffers.py deleted file mode 100644 index c7d5626551..0000000000 --- a/py/examples/plot_issue_buffers.py +++ /dev/null @@ -1,69 +0,0 @@ -from h2o_wave import main, app, Q, ui, data - - -# Array buffer - rows -ar = data(fields='price low high', size=8, rows=[ - [4, 50, 100], - [6, 100, 150], - [8, 150, 200], - [16, 350, 400], - [18, 400, 450], - [10, 200, 250], - [12, 250, 300], - [14, 300, 350], - ], pack=False) - -# Array buffer - columns -ac = data(fields='price low high', size=8, columns=[ - [4,6,8,16,18,10,12,14], - [50,100,150,350,400,200,250,300], - [100,150,200,400,450,250,300,350] - ], pack=False) - -# Cyclic buffer - rows -cr = data(fields='price low high', size=-8, rows=[ - [4, 50, 100], - [6, 100, 150], - [8, 150, 200], - [16, 350, 400], - [18, 400, 450], - [10, 200, 250], - [12, 250, 300], - [14, 300, 350], - ], pack=False) # pack=False not working - -# Map buffer - rows -mr = data(fields='price low high', rows=dict( - fst=[4, 50, 100], - snd=[6, 100, 150], - trd=[8, 150, 200], - fth=[16, 350, 400], - fih=[18, 400, 450], - sth=[10, 200, 250], - seh=[12, 250, 300], - nth=[14, 300, 350], -)) - -# Map buffer - columns -# mc = data(fields='price low high', columns=dict( -# fst=[4,6,8,16,18,10,12,14], -# snd=[50,100,150,350,400,200,250,300], -# trd=[100,150,200,400,450,250,300,350] -# )) - -@app('/demo') -async def serve(q: Q): - if not q.client.initialized: - q.page['meta'] = ui.meta_card(box='') - q.page['example'] = ui.plot_card( - box='1 1 4 5', - title='Histogram', - data=ar, - plot=ui.plot([ui.mark(type='interval', y='=price', x1='=low', x2='=high', y_min=0)]), - ) - q.page['btn'] = ui.form_card(box='5 6 2 2', items=[ui.button(name='change_data', label='Change data', primary=True)]) - q.client.initialized = True - elif q.args.change_data: - q.page['example'].data[2] = [9,160,210] - - await q.page.save() \ No newline at end of file diff --git a/ts/index.ts b/ts/index.ts index 2297122bc5..04fe8712a3 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -406,8 +406,7 @@ export function unpackByIdx(data: any, idx: U): T { ? decodeStringByIdx(data, idx) : (isData(data)) ? data.getTupByIdx(idx) - // TODO: Optimize - use for in. Object.entries() not supported in this ES version. - : data?.[idx] || data[Object.keys(data)?.[idx]] + : data?.[idx] || getObjectValueByIdx(data, idx) } const @@ -482,19 +481,26 @@ const return data }, decodeStringByIdx = (data: S, idx: U): any => { - if (data === '') return data + if (data === '') return const [t, d] = decodeType(data) try { - if (t === 'data') return JSON.parse(d)[idx] const [fields, items] = JSON.parse(d) 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 JSON.parse(d)[idx] } catch (e) { console.error(e) } - return JSON.parse(d)[idx] + }, + getObjectValueByIdx = (data: any, idx: U): T | undefined => { + let i = 0 + for (const k in data) { + if (i === idx) return data?.[k] + i++ + } + return }, keysOf = (d: Dict): S[] => { const a: S[] = [] @@ -505,7 +511,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 @@ -669,12 +674,8 @@ const return tup ? newCur(t, tup) : null }, getTupByIdx = (i: U): Rec | null => { - let idx = 0 - for (const k in tups) { - if (idx === i && tups.hasOwnProperty(k)) return t.make(tups[k]) - idx++ - } - return null + const tup = getObjectValueByIdx(tups, i) + return tup ? t.make(tup) : null }, list = (): Rec[] => { const keys = keysOf(tups) From b24dc205010c4b2bc4c4c37411b5c917a02996dc Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Mon, 13 Mar 2023 14:33:35 +0100 Subject: [PATCH 13/14] chore: cleanup --- ts/index.ts | 3 +-- ui/src/plot.tsx | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ts/index.ts b/ts/index.ts index 04fe8712a3..b34f8c0460 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -393,7 +393,6 @@ export const } export function unpack(data: any): T { - console.log(data) return (typeof data === 'string') ? decodeString(data) : (isData(data)) @@ -489,7 +488,7 @@ const if (t === 'rows') return rowToRowObj(items[idx], fields) if (t === 'cols' && items.length && items.length === fields.length) return colToRowObj(items, fields, idx) } - return JSON.parse(d)[idx] + return JSON.parse(d)?.[idx] } catch (e) { console.error(e) } diff --git a/ui/src/plot.tsx b/ui/src/plot.tsx index 96f817b6db..9507d6b085 100644 --- a/ui/src/plot.tsx +++ b/ui/src/plot.tsx @@ -1042,8 +1042,8 @@ tooltipContainer.className = 'g2-tooltip' const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalData: Rec }) => <> - {items.map(({ data, mappingData, color }: TooltipItem) => { - return Object.entries(unpackByIdx(originalData, data.idx)).map(([key, item], idx) => + {items.map(({ data, mappingData, color }: TooltipItem) => + Object.entries(unpackByIdx(originalData, data.idx)).map(([key, item], idx) =>
  • @@ -1053,7 +1053,6 @@ const PlotTooltip = ({ items, originalData }: { items: TooltipItem[], originalDa
  • ) - } )} From 26cb2f64fe955d713e914699b4e44577cb37a076 Mon Sep 17 00:00:00 2001 From: Marek Mihok Date: Tue, 14 Mar 2023 17:02:47 +0100 Subject: [PATCH 14/14] chore: parse json only once in case of non-row or non-column string data --- ts/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ts/index.ts b/ts/index.ts index b34f8c0460..6a764f9f5c 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -483,17 +483,20 @@ const if (data === '') return const [t, d] = decodeType(data) try { - const [fields, items] = JSON.parse(d) + 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 JSON.parse(d)?.[idx] + return parsedData?.[idx] } catch (e) { console.error(e) } }, getObjectValueByIdx = (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]