Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chart dev tools #2001

Merged
merged 7 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/malloy-render/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ const config: StorybookConfig = {
},
},
server: {
// Disable HMR for now, as we can't seem to get malloy core nor Lit to fully support it
// Disable HMR for now, as we can't seem to get malloy core nor web component to fully support it
hmr: false,
},
define: {
'process.env': {},
'process.env': {
'IS_STORYBOOK': true,
},
},
assetsInclude: ['/sb-preview/runtime.js'],
plugins: [viteMalloyStoriesPlugin()],
Expand Down
52 changes: 49 additions & 3 deletions packages/malloy-render/DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,44 @@ Renderers are applied based on tags parsed on an Explore or Field. This is done
1. Update the `shouldRenderAs` function that looks at the field tags and returns an enum for your renderer type
2. Update the switch statement in the `applyRenderer` function to add a case for your new renderer. Your case statement should update the `renderValue` variable using your renderer.

## Adding component CSS

The renderer is shipped as webcomponent, and any stylesheets should be appended to the ShadowDOM root instead of to the document. A utility is provided for doing this like so:

```typescript
// in file my-component.ts

// import your stylesheet as a string
import myCss from "./style.css?raw";

export function MyComponent() {
// Add your imported CSS to the ShadowRoot
const config = useConfig();
config.addCSSToShadowRoot(myCSS);

// Use your classes in your markup
return <div class="my-component-class">hello world</div>
}
```

There are certain cases where you may need to append CSS to the document; for example, when rendering a tooltip or modal that is attached to the DOM outside of the web component. You can add this document CSS like so:

```typescript
// in file my-component.ts

// import your stylesheet as a string
import myCss from "./style.css?raw";

export function MyComponent() {
// Add your imported CSS to the document
const config = useConfig();
config.addCSSToDocument(myCSS);

// Use your classes in your markup
return <div class="my-component-class">hello world</div>
}
```

## Renderer Metadata

Some of the renderers, especially charts, benefit from having metadata about the data table being rendered. This can include attributes like the min and max values in a numeric column, the number of rows in a nested table, etc. This metadata is derived in `src/component/render-result-metadata.ts`. The `getResultMetadata` function is used to create the `RenderResultMetadata` object. This object is shared throughout our SolidJS component tree using the Context api. To access the metadata, use the `useResultContext()` method in a component.
Expand Down Expand Up @@ -118,10 +156,18 @@ For examples of how to use brushIn data and export brushOut data, see the bar ch

## Debugging tips

TODO
In the Storybook, hovering any of the new charts will show a debug icon in the top right. Clicking the debug icon will open that chart with a text editor of the spec and a signal and data inspector. This UI can be used to see what is happening under the hood with the Vega chart, and allow you to experiment with changing the spec.

- signal logging
- vega introspection
You can also use the signal logger utility to log signal changes. This can be done in `chart.tsx` or `vega-chart.tsx` like so:

```typescript
import {signalLogger} from './vega-utils';

// Somewhere in component code where View is accessible, create logger and log some signals
const logger = signalLogger(view, optionalId); // Optional string id which will be logged as well

logger('signalA', 'signalB'); // Logs any value changes to signalA or signalB
```

## Generating builds

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,23 @@ import {
MalloyVegaDataRecord,
RenderResultMetadata,
VegaChartProps,
VegaSpec,
VegaPadding,
VegaSignalRef,
} from '../types';
import {getChartLayoutSettings} from '../chart-layout-settings';
import {getFieldFromRootPath, getFieldReferenceId} from '../plot/util';
import {Item, View} from 'vega';
import {
Data,
EncodeEntry,
GroupMark,
Item,
Legend,
Mark,
RectMark,
Signal,
Spec,
View,
} from 'vega';
import {renderTimeString} from '../render-time';
import {renderNumericField} from '../render-numeric-field';
import {createMeasureAxis} from '../vega/measure-axis';
Expand Down Expand Up @@ -180,8 +192,8 @@ export function generateBarChartVegaSpec(

// Spacing for bar groups, depending on whether grouped or not
// Manually calculating offsets and widths for bars because we need x highlight event targets to be full bandwidth
const xOffset: VegaSpec = {};
let xWidth: VegaSpec = {};
const xOffset: VegaSignalRef = {signal: ''};
let xWidth: EncodeEntry['width'] = {};
if (isGrouping) {
xOffset.signal = `scale('xOffset', datum.series)+bandwidth("xscale")*${
barGroupPadding / 2
Expand All @@ -193,7 +205,7 @@ export function generateBarChartVegaSpec(
}

// Create groups for each unique x value via faceting
const groupMark: VegaSpec = {
const groupMark: GroupMark = {
name: 'x_group',
from: {
facet: {
Expand Down Expand Up @@ -238,7 +250,7 @@ export function generateBarChartVegaSpec(

const BAR_FADE_OPACITY = 0.35;

const barMark: VegaSpec = {
const barMark: RectMark = {
name: 'bars',
type: 'rect',
from: {
Expand Down Expand Up @@ -289,7 +301,7 @@ export function generateBarChartVegaSpec(
},
};

const highlightMark: VegaSpec = {
const highlightMark: RectMark = {
name: 'x_highlight',
type: 'rect',
from: {
Expand Down Expand Up @@ -322,34 +334,34 @@ export function generateBarChartVegaSpec(
},
};

groupMark.marks.push(highlightMark, barMark);
groupMark.marks!.push(highlightMark, barMark);

// Source data and transforms
const valuesData: VegaSpec = {name: 'values', values: [], transform: []};
const valuesData: Data = {name: 'values', values: [], transform: []};
// For measure series, unpivot the measures into the series column
if (isMeasureSeries) {
// Pull the series values from the source record, then remap the names to remove __source
valuesData.transform.push({
valuesData.transform!.push({
type: 'fold',
fields: settings.yChannel.fields.map(f => `__source.${f}`),
as: ['series', 'y'],
});
valuesData.transform.push({
valuesData.transform!.push({
type: 'formula',
as: 'series',
expr: "replace(datum.series, '__source.', '')",
});
}
if (isStacking) {
valuesData.transform.push({
valuesData.transform!.push({
type: 'stack',
groupby: ['x'],
field: 'y',
sort: {field: 'series'},
});
}

const marks = [groupMark];
const marks: Mark[] = [groupMark];

/**************************************
*
Expand All @@ -358,7 +370,7 @@ export function generateBarChartVegaSpec(
*************************************/

// Base signals
const signals: VegaSpec[] = [
const signals: Signal[] = [
{
name: 'malloyExplore',
},
Expand Down Expand Up @@ -560,7 +572,7 @@ export function generateBarChartVegaSpec(
*
*************************************/

const spec: VegaSpec = {
const spec: Spec = {
$schema: 'https://vega.github.io/schema/vega/v5.json',
width: chartSettings.plotWidth,
height: chartSettings.plotHeight,
Expand Down Expand Up @@ -682,16 +694,16 @@ export function generateBarChartVegaSpec(
maxCharCt * 10 + 20
);

const legendSettings: VegaSpec = {
const legendSettings: Legend = {
// Provide padding around legend entries
titleLimit: legendSize - 20,
labelLimit: legendSize - 40,
padding: 8,
offset: 4,
};

spec.padding.right = legendSize;
spec.legends.push({
(spec.padding as VegaPadding).right = legendSize;
spec.legends!.push({
fill: 'color',
// No title for measure list legends
title: seriesField ? seriesField.name : '',
Expand Down
10 changes: 5 additions & 5 deletions packages/malloy-render/src/component/chart-layout-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

import {Explore, ExploreField, Field, Tag} from '@malloydata/malloy';
import {scale, locale} from 'vega';
import {scale, locale, AlignValue, TextBaselineValue} from 'vega';
import {getFieldKey, getTextWidthDOM} from './util';
import {RenderResultMetadata} from './types';
import {renderNumericField} from './render-numeric-field';
Expand All @@ -32,8 +32,8 @@ export type ChartLayoutSettings = {
plotHeight: number;
xAxis: {
labelAngle: number;
labelAlign?: string;
labelBaseline?: string;
labelAlign?: AlignValue;
labelBaseline?: TextBaselineValue;
labelLimit: number;
height: number;
titleSize: number;
Expand Down Expand Up @@ -109,8 +109,8 @@ export function getChartLayoutSettings(
let xAxisHeight = 0;
let yAxisWidth = 0;
let labelAngle = -90;
let labelAlign: string | undefined = 'right';
let labelBaseline = 'middle';
let labelAlign: AlignValue | undefined = 'right';
let labelBaseline: TextBaselineValue | undefined = 'middle';
let labelLimit = 0;
let xTitleSize = 0;
let yTitleSize = 0;
Expand Down
Loading