Skip to content

Commit 80faadd

Browse files
committed
feat: drilling, style inheritance
1 parent 67dfdc4 commit 80faadd

File tree

14 files changed

+199
-119
lines changed

14 files changed

+199
-119
lines changed

packages/malloy-render/src/component/apply-renderer.tsx

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {renderList} from './render-list';
2525
import {renderImage} from './render-image';
2626
import {Dashboard} from './dashboard/dashboard';
2727
import {LegacyChart} from './legacy-charts/legacy_chart';
28-
import {hasAny} from './tag-utils';
2928
import {renderTime} from './render-time';
3029

3130
export type RendererProps = {
@@ -36,27 +35,47 @@ export type RendererProps = {
3635
customProps?: Record<string, Record<string, unknown>>;
3736
};
3837

38+
const RENDER_TAG_LIST = [
39+
'link',
40+
'image',
41+
'cell',
42+
'list',
43+
'list_detail',
44+
'bar_chart',
45+
'line_chart',
46+
'dashboard',
47+
'scatter_chart',
48+
'shape_map',
49+
'segment_map',
50+
];
51+
52+
const CHART_TAG_LIST = ['bar_chart', 'line_chart'];
53+
54+
export function shouldRenderChartAs(tag: Tag) {
55+
const tagNamesInOrder = Object.keys(tag.properties ?? {}).reverse();
56+
return tagNamesInOrder.find(name => CHART_TAG_LIST.includes(name));
57+
}
58+
3959
export function shouldRenderAs(f: Field | Explore, tagOverride?: Tag) {
4060
const tag = tagOverride ?? f.tagParse().tag;
41-
if (!f.isExplore() && f.isAtomicField()) {
42-
if (tag.has('link')) return 'link';
43-
if (tag.has('image')) return 'image';
44-
return 'cell';
61+
const tagNamesInOrder = Object.keys(tag.properties ?? {}).reverse();
62+
for (const tagName of tagNamesInOrder) {
63+
if (RENDER_TAG_LIST.includes(tagName)) {
64+
if (['list', 'list_detail'].includes(tagName)) return 'list';
65+
if (['bar_chart', 'line_chart'].includes(tagName)) return 'chart';
66+
return tagName;
67+
}
4568
}
46-
if (hasAny(tag, 'list', 'list_detail')) return 'list';
47-
if (hasAny(tag, 'bar_chart', 'line_chart')) return 'chart';
48-
if (tag.has('dashboard')) return 'dashboard';
49-
if (hasAny(tag, 'scatter_chart')) return 'scatter_chart';
50-
if (hasAny(tag, 'shape_map')) return 'shape_map';
51-
if (hasAny(tag, 'segment_map')) return 'segment_map';
52-
else return 'table';
69+
70+
if (!f.isExplore() && f.isAtomicField()) return 'cell';
71+
return 'table';
5372
}
5473

5574
export const NULL_SYMBOL = '∅';
5675

5776
export function applyRenderer(props: RendererProps) {
58-
const {field, dataColumn, resultMetadata, tag, customProps = {}} = props;
59-
const renderAs = shouldRenderAs(field, tag);
77+
const {field, dataColumn, resultMetadata, customProps = {}} = props;
78+
const renderAs = resultMetadata.field(field).renderAs;
6079
let renderValue: JSXElement = '';
6180
const propsToPass = customProps[renderAs] || {};
6281
if (dataColumn.isNull()) {

packages/malloy-render/src/component/register-webcomponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default function registerWebComponent({
2020
modelDef: undefined,
2121
scrollEl: undefined,
2222
onClick: undefined,
23+
onDrill: undefined,
2324
vegaConfigOverride: undefined,
2425
tableConfig: undefined,
2526
dashboardConfig: undefined,

packages/malloy-render/src/component/render-result-metadata.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,24 @@ import {
3737
valueIsNumber,
3838
valueIsString,
3939
} from './util';
40-
import {hasAny} from './tag-utils';
4140
import {
4241
DataRowWithRecord,
4342
RenderResultMetadata,
4443
VegaChartProps,
4544
VegaConfigHandler,
4645
} from './types';
47-
import {NULL_SYMBOL, shouldRenderAs} from './apply-renderer';
46+
import {
47+
NULL_SYMBOL,
48+
shouldRenderAs,
49+
shouldRenderChartAs,
50+
} from './apply-renderer';
4851
import {mergeVegaConfigs} from './vega/merge-vega-configs';
4952
import {baseVegaConfig} from './vega/base-vega-config';
5053
import {renderTimeString} from './render-time';
5154
import {generateBarChartVegaSpec} from './bar-chart/generate-bar_chart-vega-spec';
5255
import {createResultStore} from './result-store/result-store';
5356
import {generateLineChartVegaSpec} from './line-chart/generate-line_chart-vega-spec';
54-
import {parse} from 'vega';
57+
import {parse, Config} from 'vega';
5558

5659
function createDataCache() {
5760
const dataCache = new WeakMap<DataColumn, QueryData>();
@@ -250,17 +253,32 @@ function populateExploreMeta(
250253
) {
251254
const fieldMeta = metadata.field(f);
252255
let vegaChartProps: VegaChartProps | null = null;
253-
if (hasAny(tag, 'bar', 'bar_chart')) {
256+
const chartType = shouldRenderChartAs(tag);
257+
if (chartType === 'bar_chart') {
254258
vegaChartProps = generateBarChartVegaSpec(f, metadata);
255-
} else if (tag.has('line_chart')) {
259+
} else if (chartType === 'line_chart') {
256260
vegaChartProps = generateLineChartVegaSpec(f, metadata);
257261
}
258262

259263
if (vegaChartProps) {
260-
const vegaConfig = mergeVegaConfigs(
264+
const vegaConfigOverride =
265+
options.getVegaConfigOverride?.(vegaChartProps.chartType) ?? {};
266+
267+
const vegaConfig: Config = mergeVegaConfigs(
261268
baseVegaConfig(),
262269
options.getVegaConfigOverride?.(vegaChartProps.chartType) ?? {}
263270
);
271+
272+
const maybeAxisYLabelFont = vegaConfigOverride['axisY']?.['labelFont'];
273+
const maybeAxisLabelFont = vegaConfigOverride['axis']?.['labelFont'];
274+
if (maybeAxisYLabelFont || maybeAxisLabelFont) {
275+
const refLineFontSignal = vegaConfig.signals?.find(
276+
signal => signal.name === 'referenceLineFont'
277+
);
278+
if (refLineFontSignal)
279+
refLineFontSignal.value = maybeAxisYLabelFont ?? maybeAxisLabelFont;
280+
}
281+
264282
fieldMeta.vegaChartProps = {
265283
...vegaChartProps,
266284
spec: {

packages/malloy-render/src/component/render.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,19 @@
2828
'calt' 1;
2929
}
3030
}
31+
32+
.malloy-copied-modal {
33+
position: fixed;
34+
background: #333;
35+
font-size: 13px;
36+
padding: 6px 12px;
37+
border-radius: 4px;
38+
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
39+
color: white;
40+
bottom: 24px;
41+
left: 100%;
42+
text-wrap: nowrap;
43+
44+
transition: all 0s;
45+
animation: modal-slide-in 2s forwards;
46+
}

packages/malloy-render/src/component/render.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {ComponentOptions, ICustomElement} from 'component-register';
2727
import {applyRenderer} from './apply-renderer';
2828
import {
2929
DashboardConfig,
30+
DrillData,
3031
MalloyClickEventPayload,
3132
TableConfig,
3233
VegaConfigHandler,
@@ -40,6 +41,7 @@ export type MalloyRenderProps = {
4041
scrollEl?: HTMLElement;
4142
modalElement?: HTMLElement;
4243
onClick?: (payload: MalloyClickEventPayload) => void;
44+
onDrill?: (drillData: DrillData) => void;
4345
vegaConfigOverride?: VegaConfigHandler;
4446
tableConfig?: Partial<TableConfig>;
4547
dashboardConfig?: Partial<DashboardConfig>;
@@ -53,6 +55,7 @@ const ConfigContext = createContext<{
5355
addCSSToShadowRoot: (css: string) => void;
5456
addCSSToDocument: (id: string, css: string) => void;
5557
onClick?: (payload: MalloyClickEventPayload) => void;
58+
onDrill?: (drillData: DrillData) => void;
5659
vegaConfigOverride?: VegaConfigHandler;
5760
modalElement?: HTMLElement;
5861
}>();
@@ -140,6 +143,7 @@ export function MalloyRender(
140143
<ConfigContext.Provider
141144
value={{
142145
onClick: props.onClick,
146+
onDrill: props.onDrill,
143147
vegaConfigOverride: props.vegaConfigOverride,
144148
element,
145149
stylesheet,
@@ -213,9 +217,14 @@ export function MalloyRenderInner(props: {
213217
};
214218

215219
return (
216-
<ResultContext.Provider value={metadata()}>
217-
{rendering().renderValue}
218-
</ResultContext.Provider>
220+
<>
221+
<ResultContext.Provider value={metadata()}>
222+
{rendering().renderValue}
223+
</ResultContext.Provider>
224+
<Show when={metadata().store.store.showCopiedModal}>
225+
<div class="malloy-copied-modal">Copied query to clipboard!</div>
226+
</Show>
227+
</>
219228
);
220229
}
221230

packages/malloy-render/src/component/result-store/result-store.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {createStore, produce, unwrap} from 'solid-js/store';
22
import {useResultContext} from '../result-context';
3+
import {DrillData, RenderResultMetadata, DimensionContextEntry} from '../types';
4+
import {Explore, Field} from '@malloydata/malloy';
35

46
interface BrushDataBase {
57
fieldRefId: string;
@@ -45,11 +47,13 @@ export type VegaBrushOutput = {
4547

4648
export interface ResultStoreData {
4749
brushes: BrushData[];
50+
showCopiedModal: boolean;
4851
}
4952

5053
export function createResultStore() {
5154
const [store, setStore] = createStore<ResultStoreData>({
5255
brushes: [],
56+
showCopiedModal: false,
5357
});
5458

5559
const getFieldBrushBySourceId = (
@@ -108,6 +112,20 @@ export function createResultStore() {
108112
return {
109113
store,
110114
applyBrushOps,
115+
triggerCopiedModal: (time = 2000) => {
116+
setStore(
117+
produce(state => {
118+
state.showCopiedModal = true;
119+
})
120+
);
121+
setTimeout(() => {
122+
setStore(
123+
produce(state => {
124+
state.showCopiedModal = false;
125+
})
126+
);
127+
}, time);
128+
},
111129
};
112130
}
113131

@@ -117,3 +135,48 @@ export function useResultStore() {
117135
const metadata = useResultContext();
118136
return metadata.store;
119137
}
138+
139+
export async function copyExplorePathQueryToClipboard({
140+
metadata,
141+
field,
142+
dimensionContext,
143+
onDrill,
144+
}: {
145+
metadata: RenderResultMetadata;
146+
field: Field;
147+
dimensionContext: DimensionContextEntry[];
148+
onDrill?: (drillData: DrillData) => void;
149+
}) {
150+
const dimensionContextEntries = dimensionContext;
151+
let explore: Field | Explore = field;
152+
while (explore.parentExplore) {
153+
explore = explore.parentExplore;
154+
}
155+
156+
const whereClause = dimensionContextEntries
157+
.map(entry => `\t\t${entry.fieldDef} = ${JSON.stringify(entry.value)}`)
158+
.join(',\n');
159+
160+
const query = `
161+
run: ${explore.name} -> {
162+
where:
163+
${whereClause}
164+
} + { select: * }`.trim();
165+
166+
const drillData: DrillData = {
167+
dimensionFilters: dimensionContextEntries,
168+
copyQueryToClipboard: async () => {
169+
try {
170+
await navigator.clipboard.writeText(query);
171+
metadata.store.triggerCopiedModal();
172+
} catch (error) {
173+
// eslint-disable-next-line no-console
174+
console.error('Failed to copy text: ', error);
175+
}
176+
},
177+
query,
178+
whereClause,
179+
};
180+
if (onDrill) onDrill(drillData);
181+
else await drillData.copyQueryToClipboard();
182+
}
Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {createContext, useContext} from 'solid-js';
22
import {createStore, SetStoreFunction, Store} from 'solid-js/store';
33
import {TableLayout} from './table-layout';
4-
import {Explore, Field} from '@malloydata/malloy';
4+
import {DimensionContextEntry} from '../types';
55

66
type TableStore = {
77
headerSizes: Record<string, number>;
@@ -11,11 +11,6 @@ type TableStore = {
1111
showCopiedModal: boolean;
1212
};
1313

14-
export type DimensionContextEntry = {
15-
fieldDef: string;
16-
value: string | number | boolean | Date;
17-
};
18-
1914
export type TableContext = {
2015
root: boolean;
2116
layout: TableLayout;
@@ -25,11 +20,6 @@ export type TableContext = {
2520
currentRow: number[];
2621
currentExplore: string[];
2722
dimensionContext: DimensionContextEntry[];
28-
copyExplorePathQueryToClipboard: (
29-
tableCtx: TableContext,
30-
field: Field,
31-
dimensionContext: DimensionContextEntry[]
32-
) => void;
3323
};
3424

3525
export const TableContext = createContext<TableContext>();
@@ -43,44 +33,3 @@ export function createTableStore() {
4333
showCopiedModal: false,
4434
});
4535
}
46-
47-
export async function copyExplorePathQueryToClipboard(
48-
tableCtx: TableContext,
49-
field: Field,
50-
dimensionContext: DimensionContextEntry[]
51-
) {
52-
const dimensionContextEntries = [
53-
...tableCtx!.dimensionContext,
54-
...dimensionContext,
55-
];
56-
let explore: Field | Explore = field;
57-
while (explore.parentExplore) {
58-
explore = explore.parentExplore;
59-
}
60-
61-
const whereClause = dimensionContextEntries
62-
.map(entry => `\t\t${entry.fieldDef} is ${JSON.stringify(entry.value)}`)
63-
.join(',\n');
64-
65-
const query = `
66-
run: ${explore.name} -> {
67-
where:
68-
${whereClause}
69-
} + { select: * }`.trim();
70-
71-
try {
72-
await navigator.clipboard.writeText(query);
73-
tableCtx.setStore(s => ({
74-
...s,
75-
showCopiedModal: true,
76-
}));
77-
setTimeout(() => {
78-
tableCtx.setStore(s => ({
79-
...s,
80-
showCopiedModal: false,
81-
}));
82-
}, 2000);
83-
} catch (error) {
84-
console.error('Failed to copy text: ', error);
85-
}
86-
}

0 commit comments

Comments
 (0)