diff --git a/package-lock.json b/package-lock.json
index 3e3be79c2..eaabd42e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5301,27 +5301,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@lit-labs/ssr-dom-shim": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz",
- "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g=="
- },
- "node_modules/@lit/context": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.0.tgz",
- "integrity": "sha512-fCyv4dsH05wCNm3AKbB+PdYbXGJd/XT8OOwo4hVmD4COq5wOWJlQreGAMDvmHZ7osqxuu06Y4nmP6ooXpN7ErA==",
- "dependencies": {
- "@lit/reactive-element": "^1.6.2 || ^2.0.0"
- }
- },
- "node_modules/@lit/reactive-element": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.1.tgz",
- "integrity": "sha512-eu50SQXHRthFwWJMp0oAFg95Rvm6MTPjxSXWuvAu7It90WVFLFpNBoIno7XOXSDvVgTrtKnUV4OLJqys2Svn4g==",
- "dependencies": {
- "@lit-labs/ssr-dom-shim": "^1.1.2"
- }
- },
"node_modules/@malloydata/db-bigquery": {
"resolved": "packages/malloy-db-bigquery",
"link": true
@@ -10124,7 +10103,8 @@
"node_modules/@types/luxon": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.4.0.tgz",
- "integrity": "sha512-oCavjEjRXuR6URJEtQm0eBdfsBiEcGBZbq21of8iGkeKxU1+1xgKuFPClaBZl2KB8ZZBSWlgk61tH6Mf+nvZVw=="
+ "integrity": "sha512-oCavjEjRXuR6URJEtQm0eBdfsBiEcGBZbq21of8iGkeKxU1+1xgKuFPClaBZl2KB8ZZBSWlgk61tH6Mf+nvZVw==",
+ "dev": true
},
"node_modules/@types/mdx": {
"version": "2.0.9",
@@ -10294,11 +10274,6 @@
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
},
- "node_modules/@types/trusted-types": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.5.tgz",
- "integrity": "sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA=="
- },
"node_modules/@types/tunnel": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
@@ -20198,34 +20173,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lit": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/lit/-/lit-3.0.2.tgz",
- "integrity": "sha512-ZoVUPGgXOQocP4OvxehEOBmC4rWB4cRYDPaz7aFmH8DFytsCi/NeACbr4C6vNPGDEC07BrhUos7uVNayDKLQ2Q==",
- "dependencies": {
- "@lit/reactive-element": "^2.0.0",
- "lit-element": "^4.0.0",
- "lit-html": "^3.0.0"
- }
- },
- "node_modules/lit-element": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.1.tgz",
- "integrity": "sha512-OxRMJem4HKZt0320HplLkBPoi4KHiEHoPHKd8Lzf07ZQVAOKIjZ32yPLRKRDEolFU1RgrQBfSHQMoxKZ72V3Kw==",
- "dependencies": {
- "@lit-labs/ssr-dom-shim": "^1.1.2",
- "@lit/reactive-element": "^2.0.0",
- "lit-html": "^3.0.0"
- }
- },
- "node_modules/lit-html": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.0.2.tgz",
- "integrity": "sha512-Q1A5lHza3bnmxoWJn6yS6vQZQdExl4fghk8W1G+jnAEdoFNYo5oeBBb/Ol7zSEdKd3TR7+r0zsJQyuWEVguiyQ==",
- "dependencies": {
- "@types/trusted-types": "^2.0.2"
- }
- },
"node_modules/load-json-file": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz",
@@ -28431,11 +28378,9 @@
"version": "0.0.137",
"license": "MIT",
"dependencies": {
- "@lit/context": "^1.1.0",
"@malloydata/malloy": "^0.0.137",
"@types/luxon": "^2.4.0",
"component-register": "^0.8.3",
- "lit": "^3.0.2",
"lodash": "^4.17.20",
"luxon": "^2.4.0",
"solid-element": "^1.8.0",
@@ -28454,6 +28399,7 @@
"@storybook/html-vite": "^7.5.0",
"@storybook/testing-library": "^0.2.2",
"@storybook/types": "^7.5.3",
+ "@types/luxon": "^2.4.0",
"esbuild": "0.19.11",
"storybook": "^7.5.0",
"vite": "^5.1.5",
diff --git a/packages/malloy-render/.storybook/main.ts b/packages/malloy-render/.storybook/main.ts
index 396f0a97d..38c2d71f9 100644
--- a/packages/malloy-render/.storybook/main.ts
+++ b/packages/malloy-render/.storybook/main.ts
@@ -53,6 +53,7 @@ const config: StorybookConfig = {
define: {
'process.env': {},
},
+ assetsInclude: ['/sb-preview/runtime.js'],
};
const finalConfig = mergeConfig(config, configOverride);
return finalConfig;
diff --git a/packages/malloy-render/package.json b/packages/malloy-render/package.json
index d8ff26f60..07b8eafcd 100644
--- a/packages/malloy-render/package.json
+++ b/packages/malloy-render/package.json
@@ -33,11 +33,9 @@
"build-types": "tsc --build --declaration --emitDeclarationOnly"
},
"dependencies": {
- "@lit/context": "^1.1.0",
"@malloydata/malloy": "^0.0.137",
"@types/luxon": "^2.4.0",
"component-register": "^0.8.3",
- "lit": "^3.0.2",
"lodash": "^4.17.20",
"luxon": "^2.4.0",
"solid-element": "^1.8.0",
@@ -56,6 +54,7 @@
"@storybook/html-vite": "^7.5.0",
"@storybook/testing-library": "^0.2.2",
"@storybook/types": "^7.5.3",
+ "@types/luxon": "^2.4.0",
"esbuild": "0.19.11",
"storybook": "^7.5.0",
"vite": "^5.1.5",
diff --git a/packages/malloy-render/src/component/bar-chart.ts b/packages/malloy-render/src/component/bar-chart.ts
deleted file mode 100644
index ec63ef4e4..000000000
--- a/packages/malloy-render/src/component/bar-chart.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2023 Google LLC
- *
- * Permission is hereby granted, free of charge, to any person obtaining
- * a copy of this software and associated documentation files
- * (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge,
- * publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-import {LitElement, html} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import './vega-chart';
-import {DataArray, ExploreField} from '@malloydata/malloy';
-import {RenderResultMetadata} from './render-result-metadata';
-import {getChartSettings} from './chart-settings';
-import {baseSpec} from './vega-lite-base-spec';
-import {valueIsNumber, valueIsString} from './util';
-
-@customElement('malloy-bar-chart')
-export class BarChart extends LitElement {
- @property({attribute: false})
- data!: DataArray;
-
- @property({attribute: false})
- metadata!: RenderResultMetadata;
-
- override render() {
- const field = this.data.field as ExploreField;
- const keys = field.allFields;
- const {tag} = field.tagParse();
- const isSpark = tag.text('size') === 'spark';
- const chartSettings = getChartSettings(field, this.metadata);
-
- const records: Partial<{barDim: string; sizeMeasure: number}>[] = [];
- for (const rec of this.data) {
- const record: Partial<{barDim: string; sizeMeasure: number}> = {};
- const xValue = rec.cell(chartSettings.xField.name).value;
- const yValue = rec.cell(chartSettings.yField.name).value;
- if (valueIsString(chartSettings.xField, xValue)) {
- record.barDim = xValue;
- }
- if (valueIsNumber(chartSettings.yField, yValue)) {
- record.sizeMeasure = yValue;
- }
- records.push(record);
- }
-
- const spec = baseSpec();
- spec.data = {
- values: records,
- };
-
- spec.layer = [
- {
- mark: {
- type: 'bar',
- width: {'band': 0.8},
- cornerRadiusEnd: 0,
- },
- encoding: {
- x: {
- field: 'barDim',
- type: 'nominal',
- sort: null, // Uses the sort order of underlying data
- axis: {
- labelAngle: chartSettings.xAxis.labelAngle,
- maxExtent: chartSettings.xAxis.height,
- labelLimit: chartSettings.xAxis.labelSize,
- title: keys[0].name,
- },
- scale: {},
- },
- y: {
- field: 'sizeMeasure',
- type: 'quantitative',
- scale: {
- domain: chartSettings.yScale.domain,
- },
- axis: {
- maxExtent: chartSettings.yAxis.width,
- labelLimit: chartSettings.yAxis.width + 10,
- tickCount: chartSettings.yAxis.tickCount,
- title: keys[1].name,
- },
- },
- // TODO: fill color from theme
- fill: {value: '#53B2C8'},
- },
- },
- ];
-
- if (isSpark) {
- spec.padding = 0;
- spec.layer[0].encoding.y.axis = null;
- } else {
- spec.padding = chartSettings.padding;
- }
-
- return html`
-
-
`;
- }
-}
diff --git a/packages/malloy-render/src/component/bar-chart/generate-bar_chart-spec.ts b/packages/malloy-render/src/component/bar-chart/generate-bar_chart-spec.ts
new file mode 100644
index 000000000..d26a75be4
--- /dev/null
+++ b/packages/malloy-render/src/component/bar-chart/generate-bar_chart-spec.ts
@@ -0,0 +1,83 @@
+import {Explore, Tag} from '@malloydata/malloy';
+import {Mark, PlotSpec, createEmptySpec} from '../plot/plot-spec';
+import {getFieldPathBetweenFields, walkFields} from '../plot/util';
+
+export function generateBarChartSpec(
+ explore: Explore,
+ tagOverride?: Tag
+): PlotSpec {
+ const tag = tagOverride ?? explore.tagParse().tag;
+ const chart = tag.tag('bar_chart') ?? tag.tag('bar');
+ if (!chart) {
+ throw new Error(
+ 'Tried to render a bar_chart, but no bar_chart tag was found'
+ );
+ }
+
+ const spec = createEmptySpec();
+
+ // Parse top level tags
+ if (chart.text('x')) {
+ spec.x.fields.push(chart.text('x')!);
+ }
+ if (chart.text('y')) {
+ spec.y.fields.push(chart.text('y')!);
+ }
+
+ // Parse embedded tags
+ const embeddedX: string[] = [];
+ const embeddedY: string[] = [];
+ walkFields(explore, field => {
+ const {tag} = field.tagParse();
+ if (tag.has('x')) {
+ embeddedX.push(getFieldPathBetweenFields(explore, field));
+ }
+ if (tag.has('y')) {
+ embeddedY.push(getFieldPathBetweenFields(explore, field));
+ }
+ });
+
+ // Add all x's found
+ embeddedX.forEach(path => {
+ spec.x.fields.push(path);
+ });
+
+ // For now, only add first y. Will handle multiple y's later
+ if (embeddedY.at(0)) {
+ spec.y.fields.push(embeddedY.at(0)!);
+ }
+
+ // If still no x or y, attempt to pick the best choice
+ if (spec.x.fields.length === 0) {
+ // Pick first string field for x. (what about dates? others? basically non numbers?)
+ const stringFields = explore.allFields.filter(
+ f => f.isAtomicField() && f.isString()
+ );
+ if (stringFields.length > 0)
+ spec.x.fields.push(getFieldPathBetweenFields(explore, stringFields[0]));
+ }
+ if (spec.y.fields.length === 0) {
+ // Pick first numeric field for y
+ const numberField = explore.allFields.find(
+ f => f.isAtomicField() && f.isNumber()
+ );
+ if (numberField)
+ spec.y.fields.push(getFieldPathBetweenFields(explore, numberField));
+ }
+
+ // Create bar mark
+ const barMark: Mark = {
+ id: 'bar',
+ type: 'bar_y',
+ x: null, // inherit from x channel
+ y: null, // inherit from y channel
+ };
+ spec.marks.push(barMark);
+
+ // Determine scale types for channels
+ // TODO: Make this derived from the fields chosen
+ spec.x.type = 'nominal';
+ spec.y.type = 'quantitative';
+
+ return spec;
+}
diff --git a/packages/malloy-render/src/component/chart-settings.ts b/packages/malloy-render/src/component/chart-settings.ts
index 7467dc69e..e3647b5f8 100644
--- a/packages/malloy-render/src/component/chart-settings.ts
+++ b/packages/malloy-render/src/component/chart-settings.ts
@@ -21,23 +21,27 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import {ExploreField, Field} from '@malloydata/malloy';
+import {Explore, ExploreField, Field} from '@malloydata/malloy';
import {scale, locale} from 'vega';
import {getFieldKey, getTextWidth} from './util';
-import {RenderResultMetadata} from './render-result-metadata';
+import {RenderResultMetadata} from './types';
export type ChartSettings = {
plotWidth: number;
plotHeight: number;
xAxis: {
labelAngle: number;
+ labelAlign?: string;
+ labelBaseline?: string;
labelSize: number;
height: number;
titleSize: number;
+ hidden: boolean;
};
yAxis: {
width: number;
tickCount?: number;
+ hidden: boolean;
};
yScale: {
domain: number[];
@@ -69,7 +73,7 @@ const CHART_SIZES = {
const ROW_HEIGHT = 28;
export function getChartSettings(
- field: ExploreField,
+ field: Explore | ExploreField,
metadata: RenderResultMetadata
): ChartSettings {
// TODO: improve logic for field extraction
@@ -94,6 +98,8 @@ export function getChartSettings(
let xAxisHeight = 0;
let yAxisWidth = 0;
let labelAngle = -90;
+ let labelAlign: string | undefined = 'right';
+ let labelBaseline = 'middle';
let labelSize = 0;
let xTitleSize = 0;
const hasXAxis = presetSize !== 'spark';
@@ -152,6 +158,8 @@ export function getChartSettings(
if (xSpacePerLabel > xAxisHeight) {
labelAngle = 0;
labelSize = xSpacePerLabel;
+ labelAlign = undefined;
+ labelBaseline = 'top';
}
}
@@ -160,28 +168,36 @@ export function getChartSettings(
const roundedUpRowHeight = Math.ceil(totalSize / ROW_HEIGHT) * ROW_HEIGHT;
xTitleSize += roundedUpRowHeight - totalSize;
+ const isSpark = tag.text('size') === 'spark';
+
return {
plotWidth: chartWidth,
plotHeight: chartHeight,
xAxis: {
labelAngle,
+ labelAlign,
+ labelBaseline,
labelSize,
height: xAxisHeight,
titleSize: xTitleSize,
+ hidden: isSpark,
},
yAxis: {
width: yAxisWidth,
tickCount: yTickCount,
+ hidden: isSpark,
},
yScale: {
domain: yDomain,
},
- padding: {
- top: topPadding,
- left: yAxisWidth,
- bottom: xAxisHeight + xTitleSize,
- right: 0,
- },
+ padding: isSpark
+ ? {top: 0, left: 0, bottom: 0, right: 0}
+ : {
+ top: topPadding + 1,
+ left: yAxisWidth,
+ bottom: xAxisHeight + xTitleSize,
+ right: 0,
+ },
xField,
yField,
get totalWidth() {
diff --git a/packages/malloy-render/src/component/chart.tsx b/packages/malloy-render/src/component/chart.tsx
new file mode 100644
index 000000000..8dba38246
--- /dev/null
+++ b/packages/malloy-render/src/component/chart.tsx
@@ -0,0 +1,22 @@
+import {Explore, ExploreField, QueryData} from '@malloydata/malloy';
+import {VegaChart} from './vega/vega-chart';
+import {RenderResultMetadata} from './types';
+
+export function Chart(props: {
+ field: Explore | ExploreField;
+ data: QueryData;
+ metadata: RenderResultMetadata;
+}) {
+ const {field, data} = props;
+ const chartProps = props.metadata.field(field).vegaChartProps!;
+ const vgSpec = structuredClone(chartProps.spec);
+ vgSpec.data[0].values = data;
+ return (
+
+ );
+}
diff --git a/packages/malloy-render/src/component/custom-elements.d.ts b/packages/malloy-render/src/component/custom-elements.d.ts
deleted file mode 100644
index 84b1d0fe5..000000000
--- a/packages/malloy-render/src/component/custom-elements.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import 'solid-js';
-import {DataArray} from '@malloydata/malloy';
-import {RenderResultMetadata} from '../render-result-metadata';
-
-// TODO: This is temporary until we move charting into Solid components
-declare module 'solid-js' {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace JSX {
- interface IntrinsicElements {
- 'malloy-bar-chart': {
- data: DataArray;
- metadata: RenderResultMetadata;
- };
- }
- }
-}
diff --git a/packages/malloy-render/src/component/plot/plot-spec.ts b/packages/malloy-render/src/component/plot/plot-spec.ts
new file mode 100644
index 000000000..32636b174
--- /dev/null
+++ b/packages/malloy-render/src/component/plot/plot-spec.ts
@@ -0,0 +1,48 @@
+type ScaleType = 'quantitative' | 'nominal';
+
+export type Channel = {
+ fields: string[];
+ type: ScaleType | null;
+};
+
+export type Mark = {
+ id: string;
+ type: string; // TODO: narrow for different mark types
+ x: string | null;
+ y: string | null;
+};
+
+export type PlotSpec = {
+ x: Channel;
+ y: Channel;
+ color: Channel;
+ fx: Channel;
+ fy: Channel;
+ marks: Mark[];
+};
+
+export function createEmptySpec(): PlotSpec {
+ return {
+ x: {
+ fields: [],
+ type: null,
+ },
+ y: {
+ fields: [],
+ type: null,
+ },
+ color: {
+ fields: [],
+ type: null,
+ },
+ fx: {
+ fields: [],
+ type: null,
+ },
+ fy: {
+ fields: [],
+ type: null,
+ },
+ marks: [],
+ };
+}
diff --git a/packages/malloy-render/src/component/plot/plot-to-vega.ts b/packages/malloy-render/src/component/plot/plot-to-vega.ts
new file mode 100644
index 000000000..deb8cbff5
--- /dev/null
+++ b/packages/malloy-render/src/component/plot/plot-to-vega.ts
@@ -0,0 +1,169 @@
+import {Explore, ExploreField} from '@malloydata/malloy';
+import {getChartSettings} from '../chart-settings';
+import {PlotSpec} from './plot-spec';
+import {RenderResultMetadata, VegaChartProps, VegaSpec} from '../types';
+
+const grayMedium = '#727883';
+const gridGray = '#E5E7EB';
+
+export function plotToVega(
+ plotSpec: PlotSpec,
+ options: {
+ field: Explore | ExploreField;
+ metadata: RenderResultMetadata;
+ }
+): VegaChartProps {
+ const chartSettings = getChartSettings(options.field, options.metadata);
+ const vegaSpec: VegaSpec = {
+ '$schema': 'https://vega.github.io/schema/vega/v5.json',
+ width: chartSettings.plotWidth,
+ height: chartSettings.plotHeight,
+ config: {
+ axisY: {
+ gridColor: gridGray,
+ tickColor: gridGray,
+ domain: false,
+
+ labelFont: 'Inter, sans-serif',
+ labelFontSize: 10,
+ labelFontWeight: 'normal',
+ labelColor: grayMedium,
+ labelPadding: 5,
+ titleColor: grayMedium,
+ titleFont: 'Inter, sans-serif',
+ titleFontSize: 12,
+ titleFontWeight: 'bold',
+ titlePadding: 10,
+ labelOverlap: false,
+ },
+ axisX: {
+ gridColor: gridGray,
+ tickColor: gridGray,
+ tickSize: 0,
+ domain: false,
+ labelFont: 'Inter, sans-serif',
+ labelFontSize: 10,
+ labelFontWeight: 'normal',
+ labelPadding: 5,
+ labelColor: grayMedium,
+ titleColor: grayMedium,
+ titleFont: 'Inter, sans-serif',
+ titleFontSize: 12,
+ titleFontWeight: 'bold',
+ titlePadding: 10,
+ },
+ view: {
+ strokeWidth: 0,
+ },
+ },
+ data: [
+ {
+ name: 'table',
+ values: [],
+ },
+ ],
+ marks: [],
+ scales: [],
+ legends: [],
+ axes: [],
+ autosize: {
+ type: 'none',
+ resize: true,
+ contains: 'content',
+ },
+ padding: chartSettings.padding,
+ };
+
+ // use spec.x/y.fields to look up all data for x axis
+ const xScale: VegaSpec = {
+ name: 'xscale',
+ type: plotSpec.x.type === 'nominal' ? 'band' : 'linear',
+ domain: {data: 'table', field: plotSpec.x.fields.at(0)},
+ range: 'width',
+ };
+
+ if (xScale.type === 'band') {
+ xScale.paddingInner = 0.1;
+ xScale.paddingOuter = 0.05;
+ xScale.round = true;
+ }
+
+ const yScale: VegaSpec = {
+ name: 'yscale',
+ type: plotSpec.y.type === 'nominal' ? 'band' : 'linear',
+ domain: chartSettings.yScale.domain ?? {
+ data: 'table',
+ fields: plotSpec.y.fields,
+ },
+ range: 'height',
+ };
+ if (yScale.type === 'linear') {
+ yScale.nice = true;
+ }
+
+ vegaSpec.scales.push(xScale);
+ vegaSpec.scales.push(yScale);
+
+ for (const plotMark of plotSpec.marks) {
+ const vegaMark: VegaSpec = {};
+
+ // Set up mark data pipeline with any transformations
+ const markData = {
+ name: plotMark.id,
+ source: 'table',
+ transform: [],
+ };
+ vegaSpec.data.push(markData);
+
+ if (plotMark.type === 'bar_y') {
+ vegaMark.type = 'rect';
+ vegaMark.from = {data: plotMark.id};
+ const xField = plotMark.x ?? plotSpec.x.fields.at(0);
+ const yField = plotMark.y ?? plotSpec.y.fields.at(0);
+ vegaMark.encode = {
+ enter: {
+ x: {scale: 'xscale', field: xField, band: 0.1},
+ width: {scale: 'xscale', band: 0.8},
+ y: {scale: 'yscale', field: yField},
+ y2: {'scale': 'yscale', 'value': 0},
+ fill: {value: '#53B2C8'},
+ },
+ };
+
+ vegaSpec.marks.push(vegaMark);
+ }
+ }
+
+ if (!chartSettings.xAxis.hidden)
+ vegaSpec.axes.push({
+ orient: 'bottom',
+ scale: 'xscale',
+ title: plotSpec.x.fields.join(', '),
+ labelAngle: chartSettings.xAxis.labelAngle,
+ labelLimit: chartSettings.xAxis.labelSize,
+ labelAlign: chartSettings.xAxis.labelAlign,
+ labelBaseline: chartSettings.xAxis.labelBaseline,
+ maxExtent: chartSettings.xAxis.height,
+ });
+
+ if (!chartSettings.yAxis.hidden)
+ vegaSpec.axes.push({
+ orient: 'left',
+ scale: 'yscale',
+ grid: true,
+ maxExtent: chartSettings.yAxis.width,
+ labelLimit: chartSettings.yAxis.width + 10,
+ tickCount: chartSettings.yAxis.tickCount ?? {signal: 'ceil(height/40)'},
+ title: [...new Set(yScale.domain.fields)]
+ .filter(s => typeof s === 'string')
+ .join(', '),
+ });
+
+ return {
+ spec: vegaSpec,
+ plotWidth: chartSettings.plotWidth,
+ plotHeight: chartSettings.plotHeight,
+ totalWidth: chartSettings.totalWidth,
+ totalHeight: chartSettings.totalHeight,
+ };
+}
diff --git a/packages/malloy-render/src/component/plot/util.ts b/packages/malloy-render/src/component/plot/util.ts
new file mode 100644
index 000000000..08117df2d
--- /dev/null
+++ b/packages/malloy-render/src/component/plot/util.ts
@@ -0,0 +1,44 @@
+import {Explore, Field} from '@malloydata/malloy';
+
+export function walkFields(e: Explore, cb: (f: Field) => void) {
+ e.allFields.forEach(f => {
+ cb(f);
+ if (f.isExplore()) {
+ walkFields(f, cb);
+ }
+ });
+}
+
+export function getFieldPathArrayFromRoot(f: Field | Explore) {
+ const paths = f.isExplore() && !f.isExploreField() ? [] : [f.name];
+ let parent = f.parentExplore;
+ while (parent?.isExploreField()) {
+ paths.unshift(parent.name);
+ parent = parent.parentExplore;
+ }
+ return paths;
+}
+
+export function getFieldPathFromRoot(f: Field | Explore) {
+ return getFieldPathArrayFromRoot(f).join('.');
+}
+
+export function getFieldPathBetweenFields(
+ parentField: Field | Explore,
+ childField: Field | Explore
+): string {
+ const parentPath = getFieldPathArrayFromRoot(parentField);
+ const childPath = getFieldPathArrayFromRoot(childField);
+ const startIndex = parentPath.length;
+
+ let i = 0;
+ while (parentPath[i]) {
+ if (parentPath[i] !== childPath[i])
+ throw new Error(
+ 'Tried to get path from parent field to child field, but parent field is not a parent of child field.'
+ );
+ i++;
+ }
+
+ return childPath.slice(startIndex).join('.');
+}
diff --git a/packages/malloy-render/src/component/render-result-metadata.ts b/packages/malloy-render/src/component/render-result-metadata.ts
index 1209d2084..048da4dce 100644
--- a/packages/malloy-render/src/component/render-result-metadata.ts
+++ b/packages/malloy-render/src/component/render-result-metadata.ts
@@ -23,95 +23,142 @@
import {
DataArray,
- DataRecord,
+ DataColumn,
Explore,
+ ExploreField,
Field,
+ QueryData,
+ QueryDataRow,
Result,
+ Tag,
} from '@malloydata/malloy';
import {getFieldKey, valueIsNumber, valueIsString} from './util';
+import {generateBarChartSpec} from './bar-chart/generate-bar_chart-spec';
+import {plotToVega} from './plot/plot-to-vega';
+import {hasAny} from './tag-utils';
+import {RenderResultMetadata} from './types';
-export interface FieldRenderMetadata {
- field: Field;
- min: number | null;
- max: number | null;
- minString: string | null;
- maxString: string | null;
- values: Set;
- maxRecordCt: number | null;
-}
-
-export interface RenderResultMetadata {
- fields: Record;
+function createDataCache() {
+ const dataCache = new WeakMap();
+ return {
+ get: (cell: DataColumn) => {
+ if (!dataCache.has(cell) && cell.isArray()) {
+ const data: QueryDataRow[] = [];
+ for (const row of cell) {
+ data.push(row.toObject());
+ }
+ dataCache.set(cell, data);
+ }
+ return dataCache.get(cell)!;
+ },
+ };
}
export function getResultMetadata(result: Result) {
- const fieldKeyMap: WeakMap = new WeakMap();
- const getCachedFieldKey = (f: Field) => {
+ const fieldKeyMap: WeakMap = new WeakMap();
+ const getCachedFieldKey = (f: Field | Explore) => {
if (fieldKeyMap.has(f)) return fieldKeyMap.get(f)!;
const fieldKey = getFieldKey(f);
fieldKeyMap.set(f, fieldKey);
return fieldKey;
};
-
+ const dataCache = createDataCache();
const metadata: RenderResultMetadata = {
fields: {},
+ fieldKeyMap,
+ getFieldKey: getCachedFieldKey,
+ field: (f: Field | Explore) => metadata.fields[getCachedFieldKey(f)],
+ getData: dataCache.get,
};
- function initFieldMeta(e: Explore) {
- for (const f of e.allFields) {
- const fieldKey = getCachedFieldKey(f);
- metadata.fields[fieldKey] = {
- field: f,
- min: null,
- max: null,
- minString: null,
- maxString: null,
- values: new Set(),
- maxRecordCt: null,
- };
- if (f.isExploreField()) {
- initFieldMeta(f);
- }
+ const rootField = result.data.field;
+ const fieldKey = metadata.getFieldKey(rootField);
+ metadata.fields[fieldKey] = {
+ field: rootField,
+ min: null,
+ max: null,
+ minString: null,
+ maxString: null,
+ values: new Set(),
+ maxRecordCt: null,
+ };
+
+ initFieldMeta(result.data.field, metadata);
+ populateFieldMeta(result.data, metadata);
+
+ Object.values(metadata.fields).forEach(m => {
+ const f = m.field;
+ // If explore, do some additional post-processing like determining chart settings
+ if (f.isExploreField()) populateExploreMeta(f, f.tagParse().tag, metadata);
+ else if (f.isExplore())
+ populateExploreMeta(f, result.tagParse().tag, metadata);
+ });
+
+ return metadata;
+}
+
+function initFieldMeta(e: Explore, metadata: RenderResultMetadata) {
+ for (const f of e.allFields) {
+ const fieldKey = metadata.getFieldKey(f);
+ metadata.fields[fieldKey] = {
+ field: f,
+ min: null,
+ max: null,
+ minString: null,
+ maxString: null,
+ values: new Set(),
+ maxRecordCt: null,
+ };
+ if (f.isExploreField()) {
+ initFieldMeta(f, metadata);
}
}
+}
- const populateFieldMeta = (
- data: DataArray,
- metadata: RenderResultMetadata,
- cb?: (row: DataRecord) => void
- ) => {
- for (const row of data) {
- cb?.(row);
- for (const f of data.field.allFields) {
- const value = f.isAtomicField() ? row.cell(f).value : undefined;
- const fieldKey = getFieldKey(f);
- const fieldMeta = metadata.fields[fieldKey];
- if (valueIsNumber(f, value)) {
- const n = value;
- fieldMeta.min = Math.min(fieldMeta.min ?? n, n);
- fieldMeta.max = Math.max(fieldMeta.max ?? n, n);
- } else if (valueIsString(f, value)) {
- const s = value;
- fieldMeta.values.add(s);
- if (!fieldMeta.minString || fieldMeta.minString.length > s.length)
- fieldMeta.minString = s;
- if (!fieldMeta.maxString || fieldMeta.maxString.length < s.length)
- fieldMeta.maxString = s;
- } else if (f.isExploreField()) {
- const data = row.cell(f) as DataArray;
- let recordCt = 0;
- populateFieldMeta(data, metadata, () => recordCt++);
- fieldMeta.maxRecordCt = Math.max(
- fieldMeta.maxRecordCt ?? recordCt,
- recordCt
- );
- }
+const populateFieldMeta = (data: DataArray, metadata: RenderResultMetadata) => {
+ let currExploreRecordCt = 0;
+ for (const row of data) {
+ currExploreRecordCt++;
+ for (const f of data.field.allFields) {
+ const value = f.isAtomicField() ? row.cell(f).value : undefined;
+ const fieldMeta = metadata.field(f);
+ if (valueIsNumber(f, value)) {
+ const n = value;
+ fieldMeta.min = Math.min(fieldMeta.min ?? n, n);
+ fieldMeta.max = Math.max(fieldMeta.max ?? n, n);
+ } else if (valueIsString(f, value)) {
+ const s = value;
+ fieldMeta.values.add(s);
+ if (!fieldMeta.minString || fieldMeta.minString.length > s.length)
+ fieldMeta.minString = s;
+ if (!fieldMeta.maxString || fieldMeta.maxString.length < s.length)
+ fieldMeta.maxString = s;
+ } else if (f.isExploreField()) {
+ const data = row.cell(f) as DataArray;
+ populateFieldMeta(data, metadata);
}
}
- };
-
- initFieldMeta(result.data.field);
- populateFieldMeta(result.data, metadata);
+ }
+ // root explore
+ const rootField = data.field;
+ const fieldMeta = metadata.field(rootField);
+ fieldMeta.maxRecordCt = Math.max(
+ fieldMeta.maxRecordCt ?? currExploreRecordCt,
+ currExploreRecordCt
+ );
+};
- return metadata;
+function populateExploreMeta(
+ f: Explore | ExploreField,
+ tag: Tag,
+ metadata: RenderResultMetadata
+) {
+ const fieldMeta = metadata.field(f);
+ if (hasAny(tag, 'bar', 'bar_chart')) {
+ const plotSpec = generateBarChartSpec(f, tag);
+ fieldMeta.vegaChartProps = plotToVega(plotSpec, {
+ field: f,
+ metadata,
+ });
+ }
}
diff --git a/packages/malloy-render/src/component/render.tsx b/packages/malloy-render/src/component/render.tsx
index 6976399db..307e00b01 100644
--- a/packages/malloy-render/src/component/render.tsx
+++ b/packages/malloy-render/src/component/render.tsx
@@ -1,10 +1,11 @@
import {ModelDef, QueryResult, Result, Tag} from '@malloydata/malloy';
-import {createEffect, createMemo} from 'solid-js';
+import {Match, Switch, createEffect, createMemo} from 'solid-js';
import {getResultMetadata} from './render-result-metadata';
import {ResultContext} from './result-context';
+import {Chart} from './chart';
import MalloyTable from './table/table';
-import './bar-chart';
import './render.css';
+import {shouldRenderAs} from './util';
export type MalloyRenderProps = {
result?: Result;
@@ -46,9 +47,23 @@ export function MalloyRender(props: MalloyRenderProps, {element}) {
}
});
+ const renderAs = () => {
+ const tag = tags().resultTag;
+ const rootField = result().resultExplore;
+ return shouldRenderAs(rootField, tag);
+ };
+
return (
-
+ }>
+
+
+
+
);
}
diff --git a/packages/malloy-render/src/component/result-context.ts b/packages/malloy-render/src/component/result-context.ts
index 5a99c4cc1..3654f0813 100644
--- a/packages/malloy-render/src/component/result-context.ts
+++ b/packages/malloy-render/src/component/result-context.ts
@@ -1,5 +1,5 @@
import {createContext, useContext} from 'solid-js';
-import {RenderResultMetadata} from './render-result-metadata';
+import {RenderResultMetadata} from './types';
export const ResultContext = createContext();
export const useResultContext = () => {
diff --git a/packages/malloy-render/src/component/table/table-layout.ts b/packages/malloy-render/src/component/table/table-layout.ts
index e4dba3f84..fc32d45c2 100644
--- a/packages/malloy-render/src/component/table/table-layout.ts
+++ b/packages/malloy-render/src/component/table/table-layout.ts
@@ -22,13 +22,10 @@
*/
import {Field} from '@malloydata/malloy';
-import {
- FieldRenderMetadata,
- RenderResultMetadata,
-} from '../render-result-metadata';
import {clamp, getFieldKey, getTextWidth} from '../util';
import {renderNumericField} from '../render-numeric-field';
-import {ChartSettings, getChartSettings} from '../chart-settings';
+import {hasAny} from '../tag-utils';
+import {FieldRenderMetadata, RenderResultMetadata} from '../types';
const MIN_COLUMN_WIDTH = 32;
const MAX_COLUMN_WIDTH = 384;
@@ -40,7 +37,6 @@ type LayoutEntry = {
metadata: FieldRenderMetadata;
width: number;
height: number | null;
- chartSettings: ChartSettings | null;
};
export type TableLayout = Record;
@@ -52,17 +48,15 @@ export function getTableLayout(metadata: RenderResultMetadata): TableLayout {
const field = fieldMeta.field;
const layoutEntry: LayoutEntry = {
metadata: fieldMeta,
- width: getColumnWidth(field, metadata),
+ width: !field.isExplore() ? getColumnWidth(field, metadata) : 0,
height: null,
- chartSettings: null,
};
const {tag} = field.tagParse();
- if (tag.has('bar') && field.isExploreField()) {
- layoutEntry.chartSettings = getChartSettings(field, metadata);
- layoutEntry.width = layoutEntry.chartSettings.totalWidth;
- layoutEntry.height = layoutEntry.chartSettings.totalHeight;
- } else if (field.isAtomicField()) {
+ if (hasAny(tag, 'bar', 'bar_chart') && field.isExploreField()) {
+ layoutEntry.width = fieldMeta.vegaChartProps!.totalWidth;
+ layoutEntry.height = fieldMeta.vegaChartProps!.totalHeight;
+ } else if (!field.isExplore() && field.isAtomicField()) {
layoutEntry.height = ROW_HEIGHT;
}
diff --git a/packages/malloy-render/src/component/table/table.tsx b/packages/malloy-render/src/component/table/table.tsx
index ffe97d3c3..b2de3c433 100644
--- a/packages/malloy-render/src/component/table/table.tsx
+++ b/packages/malloy-render/src/component/table/table.tsx
@@ -7,8 +7,15 @@ import {
Show,
Switch,
Match,
+ JSXElement,
} from 'solid-js';
-import {AtomicField, DataArray, DataRecord, Field} from '@malloydata/malloy';
+import {
+ AtomicField,
+ DataArray,
+ DataRecord,
+ ExploreField,
+ Field,
+} from '@malloydata/malloy';
import {
getFieldKey,
isFirstChild,
@@ -22,10 +29,11 @@ import {getTableLayout} from './table-layout';
import {useResultContext} from '../result-context';
import {TableContext, useTableContext} from './table-context';
import './table.css';
+import {Chart} from '../chart';
const Cell = (props: {
field: Field;
- value: string | number | Element;
+ value: JSXElement;
hideStartGutter: boolean;
hideEndGutter: boolean;
isHeader?: boolean;
@@ -104,7 +112,7 @@ const HeaderField = (props: {field: Field}) => {
const TableField = (props: {field: Field; row: DataRecord}) => {
const tableCtx = useTableContext()!;
const renderAs = shouldRenderAs(props.field);
- let renderValue: string | number | Element = '';
+ let renderValue: JSXElement = '';
if (tableCtx.pinnedHeader) renderValue = '';
else if (renderAs === 'cell') {
const resultCellValue = props.row.cell(props.field).value;
@@ -119,14 +127,15 @@ const TableField = (props: {field: Field; row: DataRecord}) => {
} else if (valueIsString(props.field, resultCellValue)) {
renderValue = resultCellValue;
}
- } else if (renderAs === 'bar-chart') {
+ } else if (renderAs === 'chart') {
const metadata = useResultContext();
renderValue = (
-
- ) as Element;
+ />
+ );
}
return (
diff --git a/packages/malloy-render/src/component/tag-utils.ts b/packages/malloy-render/src/component/tag-utils.ts
new file mode 100644
index 000000000..e061ab270
--- /dev/null
+++ b/packages/malloy-render/src/component/tag-utils.ts
@@ -0,0 +1,7 @@
+import {Tag} from '@malloydata/malloy/src';
+
+export function hasAny(tag: Tag, ...paths: Array): boolean {
+ return paths.some(path =>
+ Array.isArray(path) ? tag.has(...path) : tag.has(path)
+ );
+}
diff --git a/packages/malloy-render/src/component/types.ts b/packages/malloy-render/src/component/types.ts
new file mode 100644
index 000000000..aa57df750
--- /dev/null
+++ b/packages/malloy-render/src/component/types.ts
@@ -0,0 +1,30 @@
+import {DataColumn, Explore, Field, QueryData} from '@malloydata/malloy';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Vega does not have good TS support
+export type VegaSpec = any;
+export type VegaChartProps = {
+ spec: VegaSpec;
+ plotWidth: number;
+ plotHeight: number;
+ totalWidth: number;
+ totalHeight: number;
+};
+
+export interface FieldRenderMetadata {
+ field: Field | Explore;
+ min: number | null;
+ max: number | null;
+ minString: string | null;
+ maxString: string | null;
+ values: Set;
+ maxRecordCt: number | null;
+ vegaChartProps?: VegaChartProps;
+}
+
+export interface RenderResultMetadata {
+ fields: Record;
+ fieldKeyMap: WeakMap;
+ getFieldKey: (f: Field | Explore) => string;
+ field: (f: Field | Explore) => FieldRenderMetadata;
+ getData: (cell: DataColumn) => QueryData;
+}
diff --git a/packages/malloy-render/src/component/util.ts b/packages/malloy-render/src/component/util.ts
index 3cbac20da..ef227c20d 100644
--- a/packages/malloy-render/src/component/util.ts
+++ b/packages/malloy-render/src/component/util.ts
@@ -20,7 +20,8 @@
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import {Explore, Field} from '@malloydata/malloy';
+import {Explore, Field, Tag} from '@malloydata/malloy';
+import {hasAny} from './tag-utils';
function getLocationInParent(f: Field | Explore) {
const parent = f.parentExplore;
@@ -61,13 +62,13 @@ export function clamp(s: number, e: number, v: number) {
return Math.max(s, Math.min(e, v));
}
-export function shouldRenderAs(f: Field) {
- if (f.isAtomicField()) return 'cell';
- const {tag} = f.tagParse();
- if (tag.has('bar')) return 'bar-chart';
+export function shouldRenderAs(f: Field | Explore, tagOverride?: Tag) {
+ if (!f.isExplore() && f.isAtomicField()) return 'cell';
+ const tag = tagOverride ?? f.tagParse().tag;
+ if (hasAny(tag, 'bar_chart')) return 'chart';
else return 'table';
}
-export function getFieldKey(f: Field) {
+export function getFieldKey(f: Field | Explore) {
return JSON.stringify(f.fieldPath);
}
diff --git a/packages/malloy-render/src/component/vega-chart.ts b/packages/malloy-render/src/component/vega-chart.ts
deleted file mode 100644
index 19ac52681..000000000
--- a/packages/malloy-render/src/component/vega-chart.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2023 Google LLC
- *
- * Permission is hereby granted, free of charge, to any person obtaining
- * a copy of this software and associated documentation files
- * (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge,
- * publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-import {LitElement, html, TemplateResult, css, PropertyValues} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {View, parse} from 'vega';
-import {compile} from 'vega-lite';
-import {VegaJSON, asVegaLiteSpec, asVegaSpec} from './vega-types';
-
-@customElement('malloy-vega-chart')
-export class VegaChart extends LitElement {
- static override styles = css`
- #vis > svg {
- display: block;
- }
- `;
- @property({attribute: false})
- spec!: VegaJSON;
-
- @property({type: String})
- type: 'vega' | 'vega-lite' = 'vega';
-
- @property({type: Number})
- width?: number;
-
- @property({type: Number})
- height?: number;
-
- private el: HTMLElement | null = null;
- private view: View | null = null;
-
- setupView() {
- if (this.view) this.view.finalize();
- const vegaspec =
- this.type === 'vega-lite'
- ? compile(asVegaLiteSpec(this.spec)).spec
- : asVegaSpec(this.spec);
-
- this.view = new View(parse(vegaspec))
- .initialize(this.el!)
- .renderer('svg')
- .hover();
- if (this.width) this.view.width(this.width);
- if (this.height) this.view.height(this.height);
-
- this.view.run();
- }
-
- firstUpdated() {
- this.el = this.shadowRoot!.getElementById('vis')!;
- this.setupView();
- }
-
- protected willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('spec') || changedProperties.has('type')) {
- if (this.el) this.setupView();
- } else {
- if (
- (changedProperties.has('width') || changedProperties.has('height')) &&
- this.view
- ) {
- if (this.width) this.view.width(this.width);
- if (this.height) this.view.height(this.height);
- this.view.run();
- }
- }
- }
-
- render(): TemplateResult {
- return html` `;
- }
-}
diff --git a/packages/malloy-render/src/component/vega-lite-base-spec.ts b/packages/malloy-render/src/component/vega-lite-base-spec.ts
deleted file mode 100644
index e0058bac5..000000000
--- a/packages/malloy-render/src/component/vega-lite-base-spec.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2023 Google LLC
- *
- * Permission is hereby granted, free of charge, to any person obtaining
- * a copy of this software and associated documentation files
- * (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge,
- * publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-import {VegaJSON} from './vega-types';
-
-const grayMedium = '#727883';
-const gridGray = '#E5E7EB';
-
-export const baseSpec = (): VegaJSON => ({
- $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
- config: {
- axisY: {
- gridColor: gridGray,
- tickColor: gridGray,
- domain: false,
- labelFont: 'Inter, sans-serif',
- labelFontSize: 10,
- labelFontWeight: 'normal',
- labelColor: grayMedium,
- labelPadding: 5,
- titleColor: grayMedium,
- titleFont: 'Inter, sans-serif',
- titleFontSize: 12,
- titleFontWeight: 'bold',
- titlePadding: 10,
- labelOverlap: false,
- },
- axisX: {
- gridColor: gridGray,
- tickColor: gridGray,
- tickSize: 0,
- domain: false,
- labelFont: 'Inter, sans-serif',
- labelFontSize: 10,
- labelFontWeight: 'normal',
- labelPadding: 5,
- labelColor: grayMedium,
- titleColor: grayMedium,
- titleFont: 'Inter, sans-serif',
- titleFontSize: 12,
- titleFontWeight: 'bold',
- titlePadding: 10,
- },
- view: {
- strokeWidth: 0,
- },
- },
- params: [],
- padding: 0,
- autosize: {
- type: 'none',
- resize: true,
- contains: 'content',
- },
- // for vega-lite, if width/height is not specificed in spec it will try to autosize. Set values to prevent
- width: 1,
- height: 1,
- data: {
- values: [],
- },
- layer: [],
-});
diff --git a/packages/malloy-render/src/component/vega/vega-chart.tsx b/packages/malloy-render/src/component/vega/vega-chart.tsx
new file mode 100644
index 000000000..9cf41db55
--- /dev/null
+++ b/packages/malloy-render/src/component/vega/vega-chart.tsx
@@ -0,0 +1,38 @@
+import {createEffect} from 'solid-js';
+import {VegaJSON, asVegaLiteSpec, asVegaSpec} from '../vega-types';
+import {View, parse} from 'vega';
+import {compile} from 'vega-lite';
+
+type VegaChartProps = {
+ spec: VegaJSON;
+ type: 'vega' | 'vega-lite';
+ width?: number;
+ height?: number;
+};
+
+export function VegaChart(props: VegaChartProps) {
+ let el!: HTMLDivElement;
+
+ let view: View | null = null;
+
+ createEffect(() => {
+ if (view) view.finalize();
+ const vegaspec =
+ props.type === 'vega-lite'
+ ? compile(asVegaLiteSpec(props.spec)).spec
+ : asVegaSpec(props.spec);
+
+ view = new View(parse(vegaspec)).initialize(el).renderer('svg').hover();
+ view.run();
+ });
+
+ createEffect(() => {
+ if (view) {
+ if (props.width) view.width(props.width);
+ if (props.height) view.height(props.height);
+ view.run();
+ }
+ });
+
+ return ;
+}
diff --git a/packages/malloy-render/src/stories/bars.stories.ts b/packages/malloy-render/src/stories/bars.stories.ts
index 83dfedfa3..19be6ac13 100644
--- a/packages/malloy-render/src/stories/bars.stories.ts
+++ b/packages/malloy-render/src/stories/bars.stories.ts
@@ -49,3 +49,24 @@ export const SparksNested = {
view: 'sparks_nested',
},
};
+
+export const TestOld = {
+ args: {
+ source: 'products',
+ view: 'test',
+ },
+};
+
+export const Test = {
+ args: {
+ source: 'products',
+ view: 'topSellingBrandsTest',
+ },
+};
+
+export const NestedTest = {
+ args: {
+ source: 'products',
+ view: 'nested_test',
+ },
+};
diff --git a/packages/malloy-render/src/stories/static/bars.malloy b/packages/malloy-render/src/stories/static/bars.malloy
index 97787af62..e7c34c53a 100644
--- a/packages/malloy-render/src/stories/static/bars.malloy
+++ b/packages/malloy-render/src/stories/static/bars.malloy
@@ -3,13 +3,25 @@ source: products is duckdb.table("data/products.parquet") extend {
measure: avg_margin is avg(retail_price - cost)
dimension: product is name
- # bar
+ # bar_chart
view: topSellingBrands is {
group_by: brand
aggregate: `Sales $` is retail_price.avg()*500
limit: 10
}
+ # bar_chart
+ view: topSellingBrandsTest is {
+ group_by: brand
+ aggregate: `Sales $` is retail_price.avg()*500
+ limit: 10
+ }
+
+ view: test is {
+ nest: topSellingBrands
+ }
+
+
view: sparks is {
group_by: category
# currency
@@ -36,7 +48,7 @@ source: products is duckdb.table("data/products.parquet") extend {
limit: 5
# currency
aggregate: `Avg Retail` is retail_price.avg()
- # bar size="spark"
+ # bar_chart size="spark"
nest: `Trailing 12mo Sales` is trailing_12_sales_trend
}
}
@@ -76,22 +88,60 @@ source: products is duckdb.table("data/products.parquet") extend {
`2xl` is topSellingBrands
}
+ view: nested_test is {
+ group_by: category
+ aggregate: avg_retail is retail_price.avg()
+ nest:
+ # bar_chart size=lg
+ # size.height=220 size.width=300
+ nested_column_1 is {
+ group_by: brand
+ aggregate: avg_retail is retail_price.avg()
+ limit: 10
+ }
+ # bar_chart size=lg
+ # size.height=220 size.width=300
+ nested_column_2 is {
+ group_by: brand
+ aggregate: avg_retail is retail_price.avg()
+ limit: 10
+ }
+ nested_column_3 is {
+ group_by: brand
+ aggregate: avg_retail is retail_price.avg()
+ limit: 10
+ }
+ limit: 2
+ }
+
view: nested is {
group_by: category
aggregate: avg_retail is retail_price.avg()
nest:
- # bar size=lg
+ # bar_chart size=lg
# size.height=220 size.width=300
nested_column_1 is {
group_by: brand
aggregate: avg_retail is retail_price.avg()
limit: 10
}
+ # bar_chart size=lg
+ # size.height=220 size.width=300
+ nested_column_2 is {
+ group_by: brand
+ aggregate: avg_retail is retail_price.avg()
+ limit: 10
+ }
+ nested_column_3 is {
+ group_by: brand
+ aggregate: avg_retail is retail_price.avg()
+ limit: 10
+ }
another_nested is {
group_by: department
aggregate: avg_retail is retail_price.avg()
- # bar
+ # bar_chart
nest:
deeply_nested is {
group_by: `sku`
diff --git a/test/jest.setup.js b/test/jest.setup.js
index 0bd5e3e92..493f1d2fa 100644
--- a/test/jest.setup.js
+++ b/test/jest.setup.js
@@ -1,5 +1,7 @@
-const {JSDOM} = require('jsdom');
-const {window} = new JSDOM(``);
+const {JSDOM, VirtualConsole} = require('jsdom');
+const {window} = new JSDOM(``, {
+ virtualConsole: new VirtualConsole().sendTo(console, {omitJSDOMErrors: true}),
+});
global.document = window.document;
global.HTMLElement = window.HTMLElement;
global.customElements = window.customElements;