diff --git a/packages/malloy-render/src/component/apply-renderer.tsx b/packages/malloy-render/src/component/apply-renderer.tsx index 9bd1bd4df..1ee4681a6 100644 --- a/packages/malloy-render/src/component/apply-renderer.tsx +++ b/packages/malloy-render/src/component/apply-renderer.tsx @@ -25,7 +25,6 @@ import {renderList} from './render-list'; import {renderImage} from './render-image'; import {Dashboard} from './dashboard/dashboard'; import {LegacyChart} from './legacy-charts/legacy_chart'; -import {hasAny} from './tag-utils'; import {renderTime} from './render-time'; export type RendererProps = { @@ -36,27 +35,47 @@ export type RendererProps = { customProps?: Record>; }; +const RENDER_TAG_LIST = [ + 'link', + 'image', + 'cell', + 'list', + 'list_detail', + 'bar_chart', + 'line_chart', + 'dashboard', + 'scatter_chart', + 'shape_map', + 'segment_map', +]; + +const CHART_TAG_LIST = ['bar_chart', 'line_chart']; + +export function shouldRenderChartAs(tag: Tag) { + const tagNamesInOrder = Object.keys(tag.properties ?? {}).reverse(); + return tagNamesInOrder.find(name => CHART_TAG_LIST.includes(name)); +} + export function shouldRenderAs(f: Field | Explore, tagOverride?: Tag) { const tag = tagOverride ?? f.tagParse().tag; - if (!f.isExplore() && f.isAtomicField()) { - if (tag.has('link')) return 'link'; - if (tag.has('image')) return 'image'; - return 'cell'; + const tagNamesInOrder = Object.keys(tag.properties ?? {}).reverse(); + for (const tagName of tagNamesInOrder) { + if (RENDER_TAG_LIST.includes(tagName)) { + if (['list', 'list_detail'].includes(tagName)) return 'list'; + if (['bar_chart', 'line_chart'].includes(tagName)) return 'chart'; + return tagName; + } } - if (hasAny(tag, 'list', 'list_detail')) return 'list'; - if (hasAny(tag, 'bar_chart', 'line_chart')) return 'chart'; - if (tag.has('dashboard')) return 'dashboard'; - if (hasAny(tag, 'scatter_chart')) return 'scatter_chart'; - if (hasAny(tag, 'shape_map')) return 'shape_map'; - if (hasAny(tag, 'segment_map')) return 'segment_map'; - else return 'table'; + + if (!f.isExplore() && f.isAtomicField()) return 'cell'; + return 'table'; } export const NULL_SYMBOL = '∅'; export function applyRenderer(props: RendererProps) { - const {field, dataColumn, resultMetadata, tag, customProps = {}} = props; - const renderAs = shouldRenderAs(field, tag); + const {field, dataColumn, resultMetadata, customProps = {}} = props; + const renderAs = resultMetadata.field(field).renderAs; let renderValue: JSXElement = ''; const propsToPass = customProps[renderAs] || {}; if (dataColumn.isNull()) { diff --git a/packages/malloy-render/src/component/register-webcomponent.ts b/packages/malloy-render/src/component/register-webcomponent.ts index 77a17ff2b..e1468d1ae 100644 --- a/packages/malloy-render/src/component/register-webcomponent.ts +++ b/packages/malloy-render/src/component/register-webcomponent.ts @@ -20,6 +20,7 @@ export default function registerWebComponent({ modelDef: undefined, scrollEl: undefined, onClick: undefined, + onDrill: undefined, vegaConfigOverride: undefined, tableConfig: undefined, dashboardConfig: undefined, diff --git a/packages/malloy-render/src/component/render-result-metadata.ts b/packages/malloy-render/src/component/render-result-metadata.ts index 0581c0c9f..89b31ee9e 100644 --- a/packages/malloy-render/src/component/render-result-metadata.ts +++ b/packages/malloy-render/src/component/render-result-metadata.ts @@ -37,21 +37,24 @@ import { valueIsNumber, valueIsString, } from './util'; -import {hasAny} from './tag-utils'; import { DataRowWithRecord, RenderResultMetadata, VegaChartProps, VegaConfigHandler, } from './types'; -import {NULL_SYMBOL, shouldRenderAs} from './apply-renderer'; +import { + NULL_SYMBOL, + shouldRenderAs, + shouldRenderChartAs, +} from './apply-renderer'; import {mergeVegaConfigs} from './vega/merge-vega-configs'; import {baseVegaConfig} from './vega/base-vega-config'; import {renderTimeString} from './render-time'; import {generateBarChartVegaSpec} from './bar-chart/generate-bar_chart-vega-spec'; import {createResultStore} from './result-store/result-store'; import {generateLineChartVegaSpec} from './line-chart/generate-line_chart-vega-spec'; -import {parse} from 'vega'; +import {parse, Config} from 'vega'; function createDataCache() { const dataCache = new WeakMap(); @@ -250,17 +253,32 @@ function populateExploreMeta( ) { const fieldMeta = metadata.field(f); let vegaChartProps: VegaChartProps | null = null; - if (hasAny(tag, 'bar', 'bar_chart')) { + const chartType = shouldRenderChartAs(tag); + if (chartType === 'bar_chart') { vegaChartProps = generateBarChartVegaSpec(f, metadata); - } else if (tag.has('line_chart')) { + } else if (chartType === 'line_chart') { vegaChartProps = generateLineChartVegaSpec(f, metadata); } if (vegaChartProps) { - const vegaConfig = mergeVegaConfigs( + const vegaConfigOverride = + options.getVegaConfigOverride?.(vegaChartProps.chartType) ?? {}; + + const vegaConfig: Config = mergeVegaConfigs( baseVegaConfig(), options.getVegaConfigOverride?.(vegaChartProps.chartType) ?? {} ); + + const maybeAxisYLabelFont = vegaConfigOverride['axisY']?.['labelFont']; + const maybeAxisLabelFont = vegaConfigOverride['axis']?.['labelFont']; + if (maybeAxisYLabelFont || maybeAxisLabelFont) { + const refLineFontSignal = vegaConfig.signals?.find( + signal => signal.name === 'referenceLineFont' + ); + if (refLineFontSignal) + refLineFontSignal.value = maybeAxisYLabelFont ?? maybeAxisLabelFont; + } + fieldMeta.vegaChartProps = { ...vegaChartProps, spec: { diff --git a/packages/malloy-render/src/component/render.css b/packages/malloy-render/src/component/render.css index d430704c1..2f8b7f589 100644 --- a/packages/malloy-render/src/component/render.css +++ b/packages/malloy-render/src/component/render.css @@ -28,3 +28,19 @@ 'calt' 1; } } + +.malloy-copied-modal { + position: fixed; + background: #333; + font-size: 13px; + padding: 6px 12px; + border-radius: 4px; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; + color: white; + bottom: 24px; + left: 100%; + text-wrap: nowrap; + + transition: all 0s; + animation: modal-slide-in 2s forwards; +} diff --git a/packages/malloy-render/src/component/render.tsx b/packages/malloy-render/src/component/render.tsx index 659756d49..b05c2bed3 100644 --- a/packages/malloy-render/src/component/render.tsx +++ b/packages/malloy-render/src/component/render.tsx @@ -27,6 +27,7 @@ import {ComponentOptions, ICustomElement} from 'component-register'; import {applyRenderer} from './apply-renderer'; import { DashboardConfig, + DrillData, MalloyClickEventPayload, TableConfig, VegaConfigHandler, @@ -40,6 +41,7 @@ export type MalloyRenderProps = { scrollEl?: HTMLElement; modalElement?: HTMLElement; onClick?: (payload: MalloyClickEventPayload) => void; + onDrill?: (drillData: DrillData) => void; vegaConfigOverride?: VegaConfigHandler; tableConfig?: Partial; dashboardConfig?: Partial; @@ -53,6 +55,7 @@ const ConfigContext = createContext<{ addCSSToShadowRoot: (css: string) => void; addCSSToDocument: (id: string, css: string) => void; onClick?: (payload: MalloyClickEventPayload) => void; + onDrill?: (drillData: DrillData) => void; vegaConfigOverride?: VegaConfigHandler; modalElement?: HTMLElement; }>(); @@ -140,6 +143,7 @@ export function MalloyRender( - {rendering().renderValue} - + <> + + {rendering().renderValue} + + +
Copied query to clipboard!
+
+ ); } diff --git a/packages/malloy-render/src/component/result-store/result-store.ts b/packages/malloy-render/src/component/result-store/result-store.ts index 09ee7fe43..b2de11cf4 100644 --- a/packages/malloy-render/src/component/result-store/result-store.ts +++ b/packages/malloy-render/src/component/result-store/result-store.ts @@ -1,5 +1,7 @@ import {createStore, produce, unwrap} from 'solid-js/store'; import {useResultContext} from '../result-context'; +import {DrillData, RenderResultMetadata, DimensionContextEntry} from '../types'; +import {Explore, Field} from '@malloydata/malloy'; interface BrushDataBase { fieldRefId: string; @@ -45,11 +47,13 @@ export type VegaBrushOutput = { export interface ResultStoreData { brushes: BrushData[]; + showCopiedModal: boolean; } export function createResultStore() { const [store, setStore] = createStore({ brushes: [], + showCopiedModal: false, }); const getFieldBrushBySourceId = ( @@ -108,6 +112,20 @@ export function createResultStore() { return { store, applyBrushOps, + triggerCopiedModal: (time = 2000) => { + setStore( + produce(state => { + state.showCopiedModal = true; + }) + ); + setTimeout(() => { + setStore( + produce(state => { + state.showCopiedModal = false; + }) + ); + }, time); + }, }; } @@ -117,3 +135,48 @@ export function useResultStore() { const metadata = useResultContext(); return metadata.store; } + +export async function copyExplorePathQueryToClipboard({ + metadata, + field, + dimensionContext, + onDrill, +}: { + metadata: RenderResultMetadata; + field: Field; + dimensionContext: DimensionContextEntry[]; + onDrill?: (drillData: DrillData) => void; +}) { + const dimensionContextEntries = dimensionContext; + let explore: Field | Explore = field; + while (explore.parentExplore) { + explore = explore.parentExplore; + } + + const whereClause = dimensionContextEntries + .map(entry => `\t\t${entry.fieldDef} = ${JSON.stringify(entry.value)}`) + .join(',\n'); + + const query = ` +run: ${explore.name} -> { +where: +${whereClause} +} + { select: * }`.trim(); + + const drillData: DrillData = { + dimensionFilters: dimensionContextEntries, + copyQueryToClipboard: async () => { + try { + await navigator.clipboard.writeText(query); + metadata.store.triggerCopiedModal(); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to copy text: ', error); + } + }, + query, + whereClause, + }; + if (onDrill) onDrill(drillData); + else await drillData.copyQueryToClipboard(); +} diff --git a/packages/malloy-render/src/component/table/table-context.ts b/packages/malloy-render/src/component/table/table-context.ts index 406886891..965a9c953 100644 --- a/packages/malloy-render/src/component/table/table-context.ts +++ b/packages/malloy-render/src/component/table/table-context.ts @@ -1,7 +1,7 @@ import {createContext, useContext} from 'solid-js'; import {createStore, SetStoreFunction, Store} from 'solid-js/store'; import {TableLayout} from './table-layout'; -import {Explore, Field} from '@malloydata/malloy'; +import {DimensionContextEntry} from '../types'; type TableStore = { headerSizes: Record; @@ -11,11 +11,6 @@ type TableStore = { showCopiedModal: boolean; }; -export type DimensionContextEntry = { - fieldDef: string; - value: string | number | boolean | Date; -}; - export type TableContext = { root: boolean; layout: TableLayout; @@ -25,11 +20,6 @@ export type TableContext = { currentRow: number[]; currentExplore: string[]; dimensionContext: DimensionContextEntry[]; - copyExplorePathQueryToClipboard: ( - tableCtx: TableContext, - field: Field, - dimensionContext: DimensionContextEntry[] - ) => void; }; export const TableContext = createContext(); @@ -43,44 +33,3 @@ export function createTableStore() { showCopiedModal: false, }); } - -export async function copyExplorePathQueryToClipboard( - tableCtx: TableContext, - field: Field, - dimensionContext: DimensionContextEntry[] -) { - const dimensionContextEntries = [ - ...tableCtx!.dimensionContext, - ...dimensionContext, - ]; - let explore: Field | Explore = field; - while (explore.parentExplore) { - explore = explore.parentExplore; - } - - const whereClause = dimensionContextEntries - .map(entry => `\t\t${entry.fieldDef} is ${JSON.stringify(entry.value)}`) - .join(',\n'); - - const query = ` -run: ${explore.name} -> { -where: -${whereClause} -} + { select: * }`.trim(); - - try { - await navigator.clipboard.writeText(query); - tableCtx.setStore(s => ({ - ...s, - showCopiedModal: true, - })); - setTimeout(() => { - tableCtx.setStore(s => ({ - ...s, - showCopiedModal: false, - })); - }, 2000); - } catch (error) { - console.error('Failed to copy text: ', error); - } -} diff --git a/packages/malloy-render/src/component/table/table.css b/packages/malloy-render/src/component/table/table.css index ec663f008..ee559b26d 100644 --- a/packages/malloy-render/src/component/table/table.css +++ b/packages/malloy-render/src/component/table/table.css @@ -222,21 +222,6 @@ color: #547ce4; cursor: pointer; } -.malloy-table .copied-modal { - position: fixed; - background: #333; - font-size: 13px; - padding: 6px 12px; - border-radius: 4px; - box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; - color: white; - bottom: 24px; - left: 100%; - text-wrap: nowrap; - - transition: all 0s; - animation: modal-slide-in 2s forwards; -} @keyframes modal-slide-in { 0% { diff --git a/packages/malloy-render/src/component/table/table.tsx b/packages/malloy-render/src/component/table/table.tsx index 03816db26..89017d67b 100644 --- a/packages/malloy-render/src/component/table/table.tsx +++ b/packages/malloy-render/src/component/table/table.tsx @@ -20,19 +20,15 @@ import { import {getRangeSize, isFirstChild, isLastChild} from '../util'; import {getTableLayout} from './table-layout'; import {useResultContext} from '../result-context'; -import { - copyExplorePathQueryToClipboard, - createTableStore, - DimensionContextEntry, - TableContext, - useTableContext, -} from './table-context'; +import {createTableStore, TableContext, useTableContext} from './table-context'; import tableCss from './table.css?raw'; -import {applyRenderer, shouldRenderAs} from '../apply-renderer'; +import {applyRenderer} from '../apply-renderer'; import {isFieldHidden} from '../../tags_utils'; import {createStore, produce} from 'solid-js/store'; import {createVirtualizer, Virtualizer} from '@tanstack/solid-virtual'; import {useConfig} from '../render'; +import {DimensionContextEntry} from '../types'; +import {copyExplorePathQueryToClipboard} from '../result-store/result-store'; const IS_CHROMIUM = navigator.userAgent.toLowerCase().indexOf('chrome') >= 0; // CSS Subgrid + Sticky Positioning only seems to work reliably in Chrome @@ -241,16 +237,19 @@ const TableField = (props: { const config = useConfig(); const isDrillingEnabled = config.tableConfig().enableDrill; - + const metadata = useResultContext(); const handleClick = async evt => { evt.stopPropagation(); - if (isDrillingEnabled && !DRILL_RENDERER_IGNORE_LIST.includes(renderAs)) { - tableCtx!.copyExplorePathQueryToClipboard( - tableCtx!, - props.field, - props.dimensionContext - ); + copyExplorePathQueryToClipboard({ + metadata, + field: props.field, + dimensionContext: [ + ...tableCtx!.dimensionContext, + ...props.dimensionContext, + ], + onDrill: config.onDrill, + }); } }; @@ -318,11 +317,12 @@ const MalloyTableRoot = (_props: { else return 0; }) .filter(([key, value]) => { + const field = resultMetadata.fields[key].field; + const parentFieldRenderer = field.parentExplore + ? resultMetadata.field(field.parentExplore)?.renderAs + : null; const isNotRoot = value.depth >= 0; - const isPartOfTable = - isNotRoot && - shouldRenderAs(resultMetadata.fields[key].field.parentExplore!) === - 'table'; + const isPartOfTable = isNotRoot && parentFieldRenderer === 'table'; return isPartOfTable; }) .map(([key, value]) => ({ @@ -729,9 +729,6 @@ const MalloyTableRoot = (_props: { - -
Copied query to clipboard!
-
); }; @@ -786,7 +783,6 @@ const MalloyTable: Component<{ currentRow: [], currentExplore: props.data.field.fieldPath, dimensionContext: [], - copyExplorePathQueryToClipboard, }; }); diff --git a/packages/malloy-render/src/component/types.ts b/packages/malloy-render/src/component/types.ts index fb02517d9..ba786cd08 100644 --- a/packages/malloy-render/src/component/types.ts +++ b/packages/malloy-render/src/component/types.ts @@ -124,3 +124,15 @@ export type TableConfig = { export type DashboardConfig = { disableVirtualization: boolean; }; + +export type DrillData = { + dimensionFilters: DimensionContextEntry[]; + copyQueryToClipboard: () => Promise; + query: string; + whereClause: string; +}; + +export type DimensionContextEntry = { + fieldDef: string; + value: string | number | boolean | Date; +}; diff --git a/packages/malloy-render/src/component/vega/base-vega-config.ts b/packages/malloy-render/src/component/vega/base-vega-config.ts index 092a9d766..3dcc9bca6 100644 --- a/packages/malloy-render/src/component/vega/base-vega-config.ts +++ b/packages/malloy-render/src/component/vega/base-vega-config.ts @@ -69,4 +69,10 @@ export const baseVegaConfig: () => Config = () => ({ 'view': { strokeWidth: 0, }, + 'signals': [ + { + name: 'referenceLineFont', + value: 'Inter, sans-serif', + }, + ], }); diff --git a/packages/malloy-render/src/component/vega/measure-axis.ts b/packages/malloy-render/src/component/vega/measure-axis.ts index a66ad942c..fc9428a6f 100644 --- a/packages/malloy-render/src/component/vega/measure-axis.ts +++ b/packages/malloy-render/src/component/vega/measure-axis.ts @@ -239,7 +239,7 @@ function createAxisReferenceLines(options: AxisReferenceLineOptions) { strokeWidth: {value: 3}, fontSize: {value: 10}, fontWeight: {value: 'normal'}, - font: {value: 'Inter, sans-serif'}, + font: {signal: 'referenceLineFont'}, strokeOpacity: {value: 1}, }, update: { @@ -267,7 +267,7 @@ function createAxisReferenceLines(options: AxisReferenceLineOptions) { fill: {value: grayMedium}, fontSize: {value: 10}, fontWeight: {value: 'normal'}, - font: {value: 'Inter, sans-serif'}, + font: {signal: 'referenceLineFont'}, }, update: { y: { @@ -409,7 +409,7 @@ function createAxisReferenceLines(options: AxisReferenceLineOptions) { // strokeWidth: {value: 3}, // fontSize: {value: 10}, // fontWeight: {value: 'normal'}, -// font: {value: 'Inter, sans-serif'}, +// font: {signal: "referenceLineFont"}, // strokeOpacity: {value: 1}, // }, // update: { @@ -443,7 +443,7 @@ function createAxisReferenceLines(options: AxisReferenceLineOptions) { // fill: {value: grayMedium}, // fontSize: {value: 10}, // fontWeight: {value: 'normal'}, -// font: {value: 'Inter, sans-serif'}, +// font: {signal: "referenceLineFont"}, // }, // update: { // y: { @@ -479,7 +479,7 @@ function createAxisReferenceLines(options: AxisReferenceLineOptions) { // strokeWidth: {value: 3}, // fontSize: {value: 10}, // fontWeight: {value: 'normal'}, -// font: {value: 'Inter, sans-serif'}, +// font: {signal: "referenceLineFont"}, // strokeOpacity: {value: 1}, // }, // update: { @@ -513,7 +513,7 @@ function createAxisReferenceLines(options: AxisReferenceLineOptions) { // fill: {value: grayMedium}, // fontSize: {value: 10}, // fontWeight: {value: 'normal'}, -// font: {value: 'Inter, sans-serif'}, +// font: {signal: "referenceLineFont"}, // }, // update: { // y: { diff --git a/packages/malloy-render/src/html/html_view.ts b/packages/malloy-render/src/html/html_view.ts index 193792383..9e066ff8b 100644 --- a/packages/malloy-render/src/html/html_view.ts +++ b/packages/malloy-render/src/html/html_view.ts @@ -52,6 +52,10 @@ export class HTMLView { if (hasNextRenderer) { const el = this.document.createElement('malloy-render'); el.result = result; + const nextRendererOptions = options.nextRendererOptions ?? {}; + for (const [key, val] of Object.entries(nextRendererOptions)) { + el[key] = val; + } return el; } else { // eslint-disable-next-line no-console diff --git a/packages/malloy-render/src/html/renderer_types.ts b/packages/malloy-render/src/html/renderer_types.ts index db3d832e6..a3cd800cf 100644 --- a/packages/malloy-render/src/html/renderer_types.ts +++ b/packages/malloy-render/src/html/renderer_types.ts @@ -21,6 +21,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import {MalloyRenderProps} from '../component/render'; import {DataStyles} from './data_styles'; export interface RendererOptions { @@ -29,6 +30,7 @@ export interface RendererOptions { onDrill?: DrillFunction; titleCase?: boolean; queryTimezone?: string; + nextRendererOptions?: Partial; } export type DrillFunction = (