Merge pull request #760 from quantified-uncertainty/more-graph-settings

Add more options for distribution charts
This commit is contained in:
Ozzie Gooen 2022-07-11 12:34:04 -07:00 committed by GitHub
commit cca6ab1df0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 361 additions and 61 deletions

View File

@ -5,18 +5,15 @@ import {
distributionError, distributionError,
distributionErrorToString, distributionErrorToString,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { Vega, VisualizationSpec } from "react-vega"; import { Vega } from "react-vega";
import * as chartSpecification from "../vega-specs/spec-distributions.json";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
import { useSize } from "react-use"; import { useSize } from "react-use";
import clsx from "clsx"; import clsx from "clsx";
import { import {
linearXScale, buildVegaSpec,
logXScale, DistributionChartSpecOptions,
linearYScale, } from "../lib/distributionSpecBuilder";
expYScale,
} from "./DistributionVegaScales";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
export type DistributionPlottingSettings = { export type DistributionPlottingSettings = {
@ -24,19 +21,17 @@ export type DistributionPlottingSettings = {
showSummary: boolean; showSummary: boolean;
/** Whether to show the user graph controls (scale etc) */ /** Whether to show the user graph controls (scale etc) */
showControls: boolean; showControls: boolean;
/** Set the x scale to be logarithmic by deault */ } & DistributionChartSpecOptions;
logX: boolean;
/** Set the y scale to be exponential by deault */
expY: boolean;
};
export type DistributionChartProps = { export type DistributionChartProps = {
distribution: Distribution; distribution: Distribution;
width?: number; width?: number;
height: number; height: number;
actions?: boolean;
} & DistributionPlottingSettings; } & DistributionPlottingSettings;
export const DistributionChart: React.FC<DistributionChartProps> = ({ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const {
distribution, distribution,
height, height,
showSummary, showSummary,
@ -44,7 +39,8 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
showControls, showControls,
logX, logX,
expY, expY,
}) => { actions = false,
} = props;
const [isLogX, setLogX] = React.useState(logX); const [isLogX, setLogX] = React.useState(logX);
const [isExpY, setExpY] = React.useState(expY); const [isExpY, setExpY] = React.useState(expY);
@ -64,7 +60,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
const massBelow0 = const massBelow0 =
shape.value.continuous.some((x) => x.x <= 0) || shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.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; let widthProp = width ? width : size.width;
if (widthProp < 20) { if (widthProp < 20) {
@ -82,7 +78,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
data={{ con: shape.value.continuous, dis: shape.value.discrete }} data={{ con: shape.value.continuous, dis: shape.value.discrete }}
width={widthProp - 10} width={widthProp - 10}
height={height} height={height}
actions={false} actions={actions}
/> />
) : ( ) : (
<ErrorAlert heading="Log Domain Error"> <ErrorAlert heading="Log Domain Error">
@ -116,16 +112,6 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
return sized; return sized;
}; };
function buildVegaSpec(isLogX: boolean, isExpY: boolean): VisualizationSpec {
return {
...chartSpecification,
scales: [
isLogX ? logXScale : linearXScale,
isExpY ? expYScale : linearYScale,
],
} as VisualizationSpec;
}
interface CheckBoxProps { interface CheckBoxProps {
label: string; label: string;
onChange: (x: boolean) => void; onChange: (x: boolean) => void;

View File

@ -44,6 +44,18 @@ export interface SquiggleChartProps {
logX?: boolean; logX?: boolean;
/** Set the y scale to be exponential by deault */ /** Set the y scale to be exponential by deault */
expY?: boolean; 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 = () => {}; const defaultOnChange = () => {};
@ -65,6 +77,12 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
diagramStart = 0, diagramStart = 0,
diagramStop = 10, diagramStop = 10,
diagramCount = 100, diagramCount = 100,
tickFormat,
minX,
maxX,
color,
title,
distributionChartActions,
}) => { }) => {
const result = useSquiggle({ const result = useSquiggle({
code, code,
@ -83,6 +101,12 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
showSummary, showSummary,
logX, logX,
expY, expY,
format: tickFormat,
minX,
maxX,
color,
title,
actions: distributionChartActions,
}; };
let chartSettings = { let chartSettings = {

View File

@ -18,7 +18,7 @@ import clsx from "clsx";
import { defaultBindings, environment } from "@quri/squiggle-lang"; import { defaultBindings, environment } from "@quri/squiggle-lang";
import { SquiggleChart } from "./SquiggleChart"; import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor"; import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
@ -27,28 +27,16 @@ import { Toggle } from "./ui/Toggle";
import { Checkbox } from "./ui/Checkbox"; import { Checkbox } from "./ui/Checkbox";
import { StyledTab } from "./ui/StyledTab"; import { StyledTab } from "./ui/StyledTab";
interface PlaygroundProps { type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
defaultCode?: string; defaultCode?: string;
/** How many pixels high is the playground */ /** 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; onCodeChange?(expr: string): void;
/* When settings change */
onSettingsChange?(settings: any): void; onSettingsChange?(settings: any): void;
/** Should we show the editor? */ /** Should we show the editor? */
showEditor?: boolean; showEditor?: boolean;
} };
const schema = yup.object({}).shape({ const schema = yup.object({}).shape({
sampleCount: yup sampleCount: yup
@ -82,6 +70,12 @@ const schema = yup.object({}).shape({
showEditor: yup.boolean().required(), showEditor: yup.boolean().required(),
logX: yup.boolean().required(), logX: yup.boolean().required(),
expY: 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), showSettingsPage: yup.boolean().default(false),
diagramStart: yup.number().required().positive().integer().default(0).min(0), diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0), diagramStop: yup.number().required().positive().integer().default(10).min(0),
@ -114,7 +108,7 @@ function InputItem<T>({
}: { }: {
name: Path<T>; name: Path<T>;
label: string; label: string;
type: "number"; type: "number" | "text" | "color";
register: UseFormRegister<T>; register: UseFormRegister<T>;
}) { }) {
return ( return (
@ -122,7 +116,7 @@ function InputItem<T>({
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div> <div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input <input
type={type} type={type}
{...register(name)} {...register(name, { valueAsNumber: type === "number" })}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/> />
</label> </label>
@ -202,6 +196,11 @@ const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
name="expY" name="expY"
label="Show y scale exponentially" label="Show y scale exponentially"
/> />
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox <Checkbox
register={register} register={register}
name="showControls" name="showControls"
@ -212,6 +211,36 @@ const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
name="showSummary" name="showSummary"
label="Show summary statistics" label="Show summary statistics"
/> />
<InputItem
name="minX"
type="number"
register={register}
label="The minimum of the charted distribution domain"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="The maximum of the charted distribution domain"
/>
<InputItem
name="title"
type="text"
register={register}
label="The title shown on the distribution"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="The format that the ticks are rendered"
/>
<InputItem
name="color"
type="color"
register={register}
label="The color of the charted distribution"
/>
</div> </div>
</HeadedSection> </HeadedSection>
</div> </div>
@ -385,6 +414,12 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
showSummary = false, showSummary = false,
logX = false, logX = false,
expY = false, expY = false,
title,
minX,
maxX,
color = "#739ECC",
tickFormat = ".9~s",
distributionChartActions,
code: controlledCode, code: controlledCode,
onCodeChange, onCodeChange,
onSettingsChange, onSettingsChange,
@ -408,6 +443,12 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
showControls, showControls,
logX, logX,
expY, expY,
title,
minX,
maxX,
color,
tickFormat,
distributionChartActions,
showSummary, showSummary,
showEditor, showEditor,
leftSizePercent: 50, leftSizePercent: 50,
@ -440,15 +481,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<SquiggleChart <SquiggleChart
code={renderedCode} code={renderedCode}
environment={env} environment={env}
diagramStart={Number(vars.diagramStart)} {...vars}
diagramStop={Number(vars.diagramStop)}
diagramCount={Number(vars.diagramCount)}
height={vars.chartHeight}
showTypes={vars.showTypes}
showControls={vars.showControls}
showSummary={vars.showSummary}
logX={vars.logX}
expY={vars.expY}
bindings={defaultBindings} bindings={defaultBindings}
jsImports={imports} jsImports={imports}
/> />
@ -496,6 +529,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const withoutEditor = <div className="mt-3">{tabs}</div>; const withoutEditor = <div className="mt-3">{tabs}</div>;
console.log(vars);
return ( return (
<SquiggleContainer> <SquiggleContainer>
<StyledTab.Group> <StyledTab.Group>

View File

@ -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 && Number.isFinite(minX)) {
xScale = { ...xScale, domainMin: minX };
}
if (maxX !== undefined && Number.isFinite(maxX)) {
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;
}

View File

@ -3,7 +3,7 @@ import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
<Meta title="Squiggle/SquiggleChart" component={SquiggleChart} /> <Meta title="Squiggle/SquiggleChart" component={SquiggleChart} />
export const Template = SquiggleChart; export const Template = (props) => <SquiggleChart {...props} />;
/* /*
We have to hardcode a width here, because otherwise some interaction with We have to hardcode a width here, because otherwise some interaction with
Storybook creates an infinite loop with the internal width Storybook creates an infinite loop with the internal width