From 0c66bb057946e0573869159961040b46729c2ad8 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Fri, 24 Jun 2022 07:22:31 +0000 Subject: [PATCH 1/2] Add more options for distribution charts Changing title, color, tick format, and domain --- .../src/components/DistributionChart.tsx | 52 ++-- .../src/components/SquiggleChart.tsx | 24 ++ .../src/lib/distributionSpecBuilder.ts | 256 ++++++++++++++++++ 3 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 packages/components/src/lib/distributionSpecBuilder.ts diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 961178ff..af644d29 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -5,18 +5,15 @@ import { distributionError, distributionErrorToString, } from "@quri/squiggle-lang"; -import { Vega, VisualizationSpec } from "react-vega"; -import * as chartSpecification from "../vega-specs/spec-distributions.json"; +import { Vega } from "react-vega"; import { ErrorAlert } from "./Alert"; import { useSize } from "react-use"; import clsx from "clsx"; import { - linearXScale, - logXScale, - linearYScale, - expYScale, -} from "./DistributionVegaScales"; + buildVegaSpec, + DistributionChartSpecOptions, +} from "../lib/distributionSpecBuilder"; import { NumberShower } from "./NumberShower"; export type DistributionPlottingSettings = { @@ -24,27 +21,26 @@ export type DistributionPlottingSettings = { showSummary: boolean; /** Whether to show the user graph controls (scale etc) */ showControls: boolean; - /** Set the x scale to be logarithmic by deault */ - logX: boolean; - /** Set the y scale to be exponential by deault */ - expY: boolean; -}; +} & DistributionChartSpecOptions; export type DistributionChartProps = { distribution: Distribution; width?: number; height: number; + actions?: boolean; } & DistributionPlottingSettings; -export const DistributionChart: React.FC = ({ - distribution, - height, - showSummary, - width, - showControls, - logX, - expY, -}) => { +export const DistributionChart: React.FC = (props) => { + const { + distribution, + height, + showSummary, + width, + showControls, + logX, + expY, + actions = false, + } = props; const [isLogX, setLogX] = React.useState(logX); const [isExpY, setExpY] = React.useState(expY); @@ -64,7 +60,7 @@ export const DistributionChart: React.FC = ({ const massBelow0 = shape.value.continuous.some((x) => x.x <= 0) || shape.value.discrete.some((x) => x.x <= 0); - const spec = buildVegaSpec(isLogX, isExpY); + const spec = buildVegaSpec(props); let widthProp = width ? width : size.width; if (widthProp < 20) { @@ -82,7 +78,7 @@ export const DistributionChart: React.FC = ({ data={{ con: shape.value.continuous, dis: shape.value.discrete }} width={widthProp - 10} height={height} - actions={false} + actions={actions} /> ) : ( @@ -116,16 +112,6 @@ export const DistributionChart: React.FC = ({ return sized; }; -function buildVegaSpec(isLogX: boolean, isExpY: boolean): VisualizationSpec { - return { - ...chartSpecification, - scales: [ - isLogX ? logXScale : linearXScale, - isExpY ? expYScale : linearYScale, - ], - } as VisualizationSpec; -} - interface CheckBoxProps { label: string; onChange: (x: boolean) => void; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index f7bb7ace..e01d6920 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -41,6 +41,18 @@ export interface SquiggleChartProps { logX?: boolean; /** Set the y scale to be exponential by deault */ expY?: boolean; + /** How to format numbers on the x axis */ + tickFormat?: string; + /** Title of the graphed distribution */ + title?: string; + /** Color of the graphed distribution */ + color?: string; + /** Specify the lower bound of the x scale */ + minX?: number; + /** Specify the upper bound of the x scale */ + maxX?: number; + /** Whether to show vega actions to the user, so they can copy the chart spec */ + distributionChartActions?: boolean; } const defaultOnChange = () => {}; @@ -60,6 +72,12 @@ export const SquiggleChart: React.FC = ({ logX = false, expY = false, chartSettings = defaultChartSettings, + tickFormat, + minX, + maxX, + color, + title, + distributionChartActions, }) => { const { result } = useSquiggle({ code: squiggleString, @@ -78,6 +96,12 @@ export const SquiggleChart: React.FC = ({ showSummary, logX, expY, + format: tickFormat, + minX, + maxX, + color, + title, + actions: distributionChartActions, }; return ( diff --git a/packages/components/src/lib/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts new file mode 100644 index 00000000..b40a973d --- /dev/null +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -0,0 +1,256 @@ +import { VisualizationSpec } from "react-vega"; +import type { LogScale, LinearScale, PowScale } from "vega"; + +export type DistributionChartSpecOptions = { + /** Set the x scale to be logarithmic by deault */ + logX: boolean; + /** Set the y scale to be exponential by deault */ + expY: boolean; + /** The minimum x coordinate shown on the chart */ + minX?: number; + /** The maximum x coordinate shown on the chart */ + maxX?: number; + /** The color of the chart */ + color?: string; + /** The title of the chart */ + title?: string; + /** The formatting of the ticks */ + format?: string; +}; + +export let linearXScale: LinearScale = { + name: "xscale", + clamp: true, + type: "linear", + range: "width", + zero: false, + nice: false, + domain: { + fields: [ + { + data: "con", + field: "x", + }, + { + data: "dis", + field: "x", + }, + ], + }, +}; +export let linearYScale: LinearScale = { + name: "yscale", + type: "linear", + range: "height", + zero: false, + domain: { + fields: [ + { + data: "con", + field: "y", + }, + { + data: "dis", + field: "y", + }, + ], + }, +}; + +export let logXScale: LogScale = { + name: "xscale", + type: "log", + range: "width", + zero: false, + base: 10, + nice: false, + clamp: true, + domain: { + fields: [ + { + data: "con", + field: "x", + }, + { + data: "dis", + field: "x", + }, + ], + }, +}; + +export let expYScale: PowScale = { + name: "yscale", + type: "pow", + exponent: 0.1, + range: "height", + zero: false, + nice: false, + domain: { + fields: [ + { + data: "con", + field: "y", + }, + { + data: "dis", + field: "y", + }, + ], + }, +}; + +export function buildVegaSpec( + specOptions: DistributionChartSpecOptions +): VisualizationSpec { + let { + format = ".9~s", + color = "#739ECC", + title, + minX, + maxX, + logX, + expY, + } = specOptions; + + let xScale = logX ? logXScale : linearXScale; + if (minX !== undefined) { + xScale = { ...xScale, domainMin: minX }; + } + + if (maxX !== undefined) { + xScale = { ...xScale, domainMax: maxX }; + } + + let spec: VisualizationSpec = { + $schema: "https://vega.github.io/schema/vega/v5.json", + description: "A basic area chart example", + width: 500, + height: 100, + padding: 5, + data: [ + { + name: "con", + }, + { + name: "dis", + }, + ], + signals: [], + scales: [xScale, expY ? expYScale : linearYScale], + axes: [ + { + orient: "bottom", + scale: "xscale", + labelColor: "#727d93", + tickColor: "#fff", + tickOpacity: 0.0, + domainColor: "#fff", + domainOpacity: 0.0, + format: format, + tickCount: 10, + }, + ], + marks: [ + { + type: "area", + from: { + data: "con", + }, + encode: { + update: { + interpolate: { value: "linear" }, + x: { + scale: "xscale", + field: "x", + }, + y: { + scale: "yscale", + field: "y", + }, + y2: { + scale: "yscale", + value: 0, + }, + fill: { + value: color, + }, + fillOpacity: { + value: 1, + }, + }, + }, + }, + { + type: "rect", + from: { + data: "dis", + }, + encode: { + enter: { + width: { + value: 1, + }, + }, + update: { + x: { + scale: "xscale", + field: "x", + }, + y: { + scale: "yscale", + field: "y", + }, + y2: { + scale: "yscale", + value: 0, + }, + fill: { + value: "#2f65a7", + }, + }, + }, + }, + { + type: "symbol", + from: { + data: "dis", + }, + encode: { + enter: { + shape: { + value: "circle", + }, + size: [{ value: 100 }], + tooltip: { + signal: "datum.y", + }, + }, + update: { + x: { + scale: "xscale", + field: "x", + }, + y: { + scale: "yscale", + field: "y", + }, + fill: { + value: "#1e4577", + }, + }, + }, + }, + ], + }; + if (title) { + spec = { + ...spec, + title: { + text: title, + }, + }; + } + + return spec; +} From 6973f70e4cdda091fa65bc941b67ef34434e3964 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Mon, 11 Jul 2022 11:22:21 +1000 Subject: [PATCH 2/2] Add graph settings to playground --- .../src/components/SquiggleChart.tsx | 47 +++++----- .../src/components/SquigglePlayground.tsx | 88 +++++++++++++------ .../src/lib/distributionSpecBuilder.ts | 4 +- .../src/stories/SquiggleChart.stories.mdx | 2 +- 4 files changed, 88 insertions(+), 53 deletions(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index cd480e69..f9f2b368 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -60,29 +60,30 @@ export interface SquiggleChartProps { const defaultOnChange = () => {}; -export const SquiggleChart: React.FC = React.memo(({ - code = "", - environment, - onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here - height = 200, - bindings = defaultBindings, - jsImports = defaultImports, - showSummary = false, - width, - showTypes = false, - showControls = false, - logX = false, - expY = false, - diagramStart = 0, - diagramStop = 10, - diagramCount = 100, - tickFormat, - minX, - maxX, - color, - title, - distributionChartActions, -}) => { +export const SquiggleChart: React.FC = React.memo( + ({ + code = "", + environment, + onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here + height = 200, + bindings = defaultBindings, + jsImports = defaultImports, + showSummary = false, + width, + showTypes = false, + showControls = false, + logX = false, + expY = false, + diagramStart = 0, + diagramStop = 10, + diagramCount = 100, + tickFormat, + minX, + maxX, + color, + title, + distributionChartActions, + }) => { const result = useSquiggle({ code, bindings, diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 301d8892..534be721 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -18,7 +18,7 @@ import clsx from "clsx"; import { defaultBindings, environment } from "@quri/squiggle-lang"; -import { SquiggleChart } from "./SquiggleChart"; +import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart"; import { CodeEditor } from "./CodeEditor"; import { JsonEditor } from "./JsonEditor"; import { ErrorAlert, SuccessAlert } from "./Alert"; @@ -27,28 +27,16 @@ import { Toggle } from "./ui/Toggle"; import { Checkbox } from "./ui/Checkbox"; import { StyledTab } from "./ui/StyledTab"; -interface PlaygroundProps { +type PlaygroundProps = SquiggleChartProps & { /** The initial squiggle string to put in the playground */ defaultCode?: string; /** How many pixels high is the playground */ - height?: number; - /** Whether to show the types of outputs in the playground */ - showTypes?: boolean; - /** Whether to show the log scale controls in the playground */ - showControls?: boolean; - /** Whether to show the summary table in the playground */ - showSummary?: boolean; - /** Whether to log the x coordinate on distribution charts */ - logX?: boolean; - /** Whether to exp the y coordinate on distribution charts */ - expY?: boolean; - /** If code is set, component becomes controlled */ - code?: string; onCodeChange?(expr: string): void; + /* When settings change */ onSettingsChange?(settings: any): void; /** Should we show the editor? */ showEditor?: boolean; -} +}; const schema = yup.object({}).shape({ sampleCount: yup @@ -82,6 +70,12 @@ const schema = yup.object({}).shape({ showEditor: yup.boolean().required(), logX: yup.boolean().required(), expY: yup.boolean().required(), + tickFormat: yup.string().default(".9~s"), + title: yup.string(), + color: yup.string().default("#739ECC").required(), + minX: yup.number(), + maxX: yup.number(), + distributionChartActions: yup.boolean(), showSettingsPage: yup.boolean().default(false), diagramStart: yup.number().required().positive().integer().default(0).min(0), diagramStop: yup.number().required().positive().integer().default(10).min(0), @@ -114,7 +108,7 @@ function InputItem({ }: { name: Path; label: string; - type: "number"; + type: "number" | "text" | "color"; register: UseFormRegister; }) { return ( @@ -122,7 +116,7 @@ function InputItem({
{label}
@@ -202,6 +196,11 @@ const ViewSettings: React.FC<{ register: UseFormRegister }> = ({ name="expY" label="Show y scale exponentially" /> + }> = ({ name="showSummary" label="Show summary statistics" /> + + + + + @@ -385,6 +414,12 @@ export const SquigglePlayground: FC = ({ showSummary = false, logX = false, expY = false, + title, + minX, + maxX, + color = "#739ECC", + tickFormat = ".9~s", + distributionChartActions, code: controlledCode, onCodeChange, onSettingsChange, @@ -408,6 +443,12 @@ export const SquigglePlayground: FC = ({ showControls, logX, expY, + title, + minX, + maxX, + color, + tickFormat, + distributionChartActions, showSummary, showEditor, leftSizePercent: 50, @@ -440,15 +481,7 @@ export const SquigglePlayground: FC = ({ @@ -496,6 +529,7 @@ export const SquigglePlayground: FC = ({ const withoutEditor =
{tabs}
; + console.log(vars); return ( diff --git a/packages/components/src/lib/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts index b40a973d..4286dbdb 100644 --- a/packages/components/src/lib/distributionSpecBuilder.ts +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -114,11 +114,11 @@ export function buildVegaSpec( } = specOptions; let xScale = logX ? logXScale : linearXScale; - if (minX !== undefined) { + if (minX !== undefined && Number.isFinite(minX)) { xScale = { ...xScale, domainMin: minX }; } - if (maxX !== undefined) { + if (maxX !== undefined && Number.isFinite(maxX)) { xScale = { ...xScale, domainMax: maxX }; } diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index 2483c985..2febfb6f 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -3,7 +3,7 @@ import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; -export const Template = SquiggleChart; +export const Template = (props) => ; /* We have to hardcode a width here, because otherwise some interaction with Storybook creates an infinite loop with the internal width