33In this demo, we build an app that showcases the query capabilities of TinyBase
44v2.0, grouping and sorting dimensional data for lightweight analytical usage.
55
6+ We've also updated it to use parameterized queries to take advantage of TinyBase
7+ v7.2.
8+
69The data from this demo is derived from ` cars.json ` in the [ Vega
710datasets] ( https://github.com/vega/vega-datasets ) - thank you [ UW Interactive
811Data Lab] ( https://idl.cs.washington.edu/ ) !
@@ -63,10 +66,12 @@ import {
6366 Provider ,
6467 useCreateQueries ,
6568 useCreateStore ,
69+ useParamValue ,
6670 useQueries ,
6771 useResultCell ,
6872 useResultSortedRowIds ,
6973 useResultTable ,
74+ useSetParamValueCallback ,
7075} from ' tinybase/ui-react' ;
7176import {ResultSortedTableInHtmlTable } from ' tinybase/ui-react-dom' ;
7277import {Inspector } from ' tinybase/ui-react-inspector' ;
@@ -130,10 +135,34 @@ app is rendered.
130135``` jsx
131136const App = () => {
132137 const store = useCreateStore (createStore);
133- const queries = useCreateQueries (store, createQueries);
134- // ...
138+ const queries = useCreateQueries (store, () =>
139+ createQueries (store)
140+ // ...
141+ ` ` `
142+
143+ Since v7.2, we can define a single parameterized query for the whole application
144+ and then simply pass in new params as the user interacts with the UI. We're
145+ going to call this master query ` cars` :
146+
147+ ` ` ` js
148+ // ...
149+ .setQueryDefinition (' cars' , ' cars' , ({select, where, group, param}) => {
150+ param (' dimensions' ).forEach ((cellId ) => select (cellId));
151+ param (' measures' ).forEach ((cellId ) => {
152+ select (cellId);
153+ group (cellId, AGGREGATES [param (' aggregate' )]);
154+ });
155+ }, {
156+ dimensions: [' Manufacturer' ],
157+ measures: [' MPG' , ' Horsepower' ],
158+ aggregate: ' Average' ,
159+ }),
160+ );
135161` ` `
136162
163+ We will be able to set the 'dimensions', 'measures', and 'aggregate' params
164+ based on user interactions.
165+
137166This application depends on loading data into the main Store the first time it
138167is rendered. We do this by having an ` isLoading` flag in the application's
139168state, and setting it to ` false ` only once the asynchronous loading sequence in
@@ -242,31 +271,43 @@ left hand side that allows the user to select dimensions, measures, and the
242271aggregation to be used.
243272
244273The ` Body ` component wraps the sidebar and the field selection state, and then
245- the ` ResultGraph ` and ` ResultTable ` components render the two modes.
246-
247- The state of the application is based on selected dimensions and measures, which
248- aggregate is used, and whether the data is rendered as a table. This is
249- set up with the following hooks:
274+ the ` ResultGraph ` and ` ResultTable ` components render the two modes. We start
275+ off with a state variable indicating whether the data is rendered as a table
276+ (` true ` ) or a graph (` false ` ):
250277
251278``` js
252279const Body = () => {
253- const [dimensions , setDimensions ] = useState ([' Manufacturer' ]);
254- const [measures , setMeasures ] = useState ([' MPG' , ' Horsepower' ]);
255- const [aggregate , setAggregate ] = useState (' Average' );
256- const [showTable , setAsGraph ] = useState (false );
280+ const [showTable , setShowTable ] = useState (false );
257281 // ...
258282` ` `
259283
260- The first three of these state variables - ` dimensions` , ` measures` , and
261- ` aggregate` - are used to create a query from the ` cars` Table, and group the
262- fields accordingly. We run the memoized creation of the query in a separate
263- hook (which we'll come to shortly), and get its Id so we can use it in the UI:
284+ The state of the application's result rendering is based on selected dimensions
285+ and measures, and which aggregate is used. These state variables - ` dimensions` ,
286+ ` measures` , and ` aggregate` - are used as the params to the ` cars` query against
287+ the ` cars` Table, and are used to group the fields accordingly.
288+
289+ Let's create three state-like pairs containing the values of these params and
290+ callbacks that will be used set them in the Queries object whenever they change
291+ in the application's sidebar:
264292
265293` ` ` js
266- const queryId = useBuildQuery (dimensions, measures, aggregate);
294+ const [dimensions , setDimensions ] = [
295+ useParamValue (' cars' , ' dimensions' ),
296+ useSetParamValueCallback (' cars' , ' dimensions' , (dimensions ) => dimensions),
297+ ];
298+
299+ const [measures , setMeasures ] = [
300+ useParamValue (' cars' , ' measures' ),
301+ useSetParamValueCallback (' cars' , ' measures' , (measures ) => measures),
302+ ];
303+
304+ const [aggregate , setAggregate ] = [
305+ useParamValue (' cars' , ' aggregate' ),
306+ useSetParamValueCallback (' cars' , ' aggregate' , (aggregate ) => aggregate),
307+ ];
267308` ` `
268309
269- Here we render the left-hand sidebar, showing the available dimensions,
310+ Next we render the left-hand sidebar, showing the available dimensions,
270311measures, and aggregates. We also show a toggle for the tabular view, and offer
271312a link to the source of the data. (We'll also cover the simple ` Select`
272313component later.)
@@ -301,7 +342,7 @@ return (
301342 id= " showTable"
302343 type= " checkbox"
303344 checked= {showTable}
304- onChange= {useCallback (({target}) => setAsGraph (target .checked ), [])}
345+ onChange= {useCallback (({target}) => setShowTable (target .checked ), [])}
305346 / >
306347 < label for = " showTable" > Show table< / label>
307348 < br / >
@@ -311,57 +352,25 @@ return (
311352 < / a>
312353 < / small>
313354 < / aside>
314- {/* ... */ }
315355` ` `
316356
317357We complete the ` Body` component with a simple toggle between the two main
318358views, which of course we will also shortly explore in detail. Both take the
319- query Id and the columns to display:
359+ list of columns to display:
320360
321361` ` ` jsx
322- {/* ... */ }
323- {showTable ? (
324- < ResultTable
325- queryId= {queryId}
326- columns= {[... dimensions, ... measures]}
327- / >
328- ) : (
329- < ResultGraph
330- queryId= {queryId}
331- dimensions= {dimensions}
332- measures= {measures}
333- / >
334- )}
362+ {
363+ showTable ? (
364+ < ResultTable columns= {[... dimensions, ... measures]} / >
365+ ) : (
366+ < ResultGraph dimensions= {dimensions} measures= {measures} / >
367+ )
368+ }
335369 < / >
336370 );
337371};
338372```
339373
340- That's the main outer structure of the application, but a lot of the magic is in
341- the ` useBuildQuery ` hook we mentioned earlier. Whenever the dimensions,
342- measures, or aggregate selections change, we want to run a new query on the data
343- accordingly. Here's the implementation:
344-
345- ``` js
346- const useBuildQuery = (dimensions , measures , aggregate ) =>
347- useMemo (() => {
348- useQueries ().setQueryDefinition (' query' , ' cars' , ({select, group}) => {
349- dimensions .forEach ((cellId ) => select (cellId));
350- measures .forEach ((cellId ) => {
351- select (cellId);
352- group (cellId, AGGREGATES [aggregate]);
353- });
354- });
355- return ' query' ;
356- }, [dimensions, measures, aggregate]);
357- ```
358-
359- The whole hook is memoized so that the query is not run every time the ` Body `
360- component is run. The query is (imaginatively) called ` query ` , and is against
361- the ` cars ` Table in the main Store. The query simply selects every selected
362- dimension, and every selected measure. Each measure is then grouped with the
363- selected aggregate.
364-
365374And that's it! Enough to query the dataset such that both the graphical and
366375tabular views work. We'll dive into _ their_ implementations next.
367376
@@ -409,9 +418,9 @@ new ui-react-dom module straight out of the box. The only extra step is
409418transforming the array of selected columns into the customCells prop to render.
410419
411420``` jsx
412- const ResultTable = ({queryId, columns}) => (
421+ const ResultTable = ({columns}) => (
413422 < ResultSortedTableInHtmlTable
414- queryId= {queryId}
423+ queryId= " cars "
415424 sortOnClick= {true }
416425 paginator= {true }
417426 limit= {10 }
@@ -433,8 +442,8 @@ cellId as className so we can color the left-hand border.
433442
434443``` jsx
435444// ...
436- const CustomCell = ({queryId, rowId, cellId}) => {
437- const cell = useResultCell (queryId , rowId, cellId);
445+ const CustomCell = ({rowId, cellId}) => {
446+ const cell = useResultCell (' cars ' , rowId, cellId);
438447 return (
439448 < span className= {cellId}> {Number .isFinite (cell) ? round (cell) : cell}< / span>
440449 );
@@ -498,7 +507,7 @@ size of the component on the screen. To track this, we use the browser's
498507` ResizeObserver ` API, and set width and height state variables whenever it changes:
499508
500509``` jsx
501- const ResultGraph = ({queryId, dimensions, measures}) => {
510+ const ResultGraph = ({dimensions, measures}) => {
502511 const ref = useRef (null );
503512 const [{width = 0 , height = 0 }, setDimensions] = useState ({});
504513 useEffect (() => {
@@ -518,11 +527,7 @@ present so we can decide how high to make the y-axis:
518527
519528``` jsx
520529// ...
521- const [xAllLabels , yValueSets , yMax ] = useGraphData (
522- queryId,
523- dimensions,
524- measures,
525- );
530+ const [xAllLabels , yValueSets , yMax ] = useGraphData (dimensions, measures);
526531// ...
527532```
528533
@@ -613,7 +618,6 @@ hidden) `<circle />` and `<text />` elements tht serve as hover-over labels for
613618each data point:
614619
615620` ` ` jsx
616- {/* ... */ }
617621 {yValueSets .map ((yValueSet , s ) => (
618622 < g className= {measures[s]} key= {s}>
619623 < path
@@ -722,9 +726,9 @@ Note that the Row Ids are sorted according to the values of the first measure
722726selected. In a real analytical scenario, this would be better to be configurable.
723727
724728``` js
725- const useGraphData = (queryId , dimensions , measures ) => {
726- const table = useResultTable (queryId );
727- const sortedRowIds = useResultSortedRowIds (queryId , measures[0 ] ?? undefined );
729+ const useGraphData = (dimensions , measures ) => {
730+ const table = useResultTable (' cars ' );
731+ const sortedRowIds = useResultSortedRowIds (' cars ' , measures[0 ] ?? undefined );
728732 return useMemo (() => {
729733 const yAll = [1 ];
730734 const xAllLabels = [];
0 commit comments