diff --git a/.changeset/hungry-cougars-hide.md b/.changeset/hungry-cougars-hide.md new file mode 100644 index 0000000000..9032630bda --- /dev/null +++ b/.changeset/hungry-cougars-hide.md @@ -0,0 +1,6 @@ +--- +"@quri/squiggle-lang": patch +"@quri/squiggle-components": patch +--- + +Added title to all plots, and to scales for xAxisLabel and yAxisLabel. Added validation for tickFormat. diff --git a/packages/components/src/components/DistributionsChart/SummaryTable.tsx b/packages/components/src/components/DistributionsChart/SummaryTable.tsx index 78d3d9d9c5..23b958ee69 100644 --- a/packages/components/src/components/DistributionsChart/SummaryTable.tsx +++ b/packages/components/src/components/DistributionsChart/SummaryTable.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { FC, PropsWithChildren } from "react"; +import * as d3 from "d3"; import { XIcon } from "@heroicons/react/solid/esm/index.js"; import { @@ -30,6 +31,7 @@ type SummaryTableRowProps = { name: string; showName: boolean; environment: Env; + tickFormat: string | undefined; }; const percentiles = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95]; @@ -39,6 +41,7 @@ const SummaryTableRow: FC = ({ name, showName, environment, + tickFormat, }) => { const mean = distribution.mean(environment); const stdev = distribution.stdev(environment); @@ -47,11 +50,19 @@ const SummaryTableRow: FC = ({ distribution.inv(environment, percentile) ); + const formatNumber = (number: number) => { + if (tickFormat) { + return d3.format(tickFormat)(number); + } else { + return ; + } + }; + const unwrapResult = ( x: result ): React.ReactNode => { if (x.ok) { - return ; + return formatNumber(x.value); } else { return ( @@ -64,9 +75,7 @@ const SummaryTableRow: FC = ({ return ( {showName && {name}} - - - + {formatNumber(mean)} {unwrapResult(stdev)} {percentileValues.map((value, i) => ( {unwrapResult(value)} @@ -82,6 +91,7 @@ type SummaryTableProps = { export const SummaryTable: FC = ({ plot, environment }) => { const showNames = plot.distributions.some((d) => d.name); + const tickFormat = plot.xScale?.tickFormat; return ( @@ -102,6 +112,7 @@ export const SummaryTable: FC = ({ plot, environment }) => { name={dist.name ?? dist.distribution.toString()} showName={showNames} environment={environment} + tickFormat={tickFormat} /> ))} diff --git a/packages/components/src/components/DistributionsChart/index.tsx b/packages/components/src/components/DistributionsChart/index.tsx index b26c6604c5..83090bfb10 100644 --- a/packages/components/src/components/DistributionsChart/index.tsx +++ b/packages/components/src/components/DistributionsChart/index.tsx @@ -34,6 +34,7 @@ import { Point } from "../../lib/draw/types.js"; import { DrawContext } from "../../lib/hooks/useCanvas.js"; import { sqScaleToD3 } from "../../lib/d3/index.js"; import { adjustPdfHeightToScale } from "./utils.js"; +import { PlotTitle } from "../PlotTitle.js"; export type DistributionsChartProps = { plot: SqDistributionsPlot; @@ -73,14 +74,11 @@ const InnerDistributionsChart: FC<{ const legendItemHeight = 16; const sampleBarHeight = 5; - const showTitle = !!plot.title; - const titleHeight = showTitle ? 20 : 4; const legendHeight = isMulti ? legendItemHeight * shapes.length : 0; const _showSamplesBar = showSamplesBar && samples.length; const samplesFooterHeight = _showSamplesBar ? 10 : 0; - const height = - innerHeight + legendHeight + titleHeight + samplesFooterHeight + 30; + const height = innerHeight + legendHeight + samplesFooterHeight + 34; const { xScale, yScale } = useMemo(() => { const xScale = sqScaleToD3(plot.xScale); @@ -117,7 +115,7 @@ const InnerDistributionsChart: FC<{ suggestedPadding: { left: 10, right: 10, - top: 10 + legendHeight + titleHeight, + top: 10 + legendHeight, bottom: 20 + samplesFooterHeight, }, xScale, @@ -125,23 +123,14 @@ const InnerDistributionsChart: FC<{ hideYAxis: true, drawTicks: true, xTickFormat: plot.xScale.tickFormat, + xAxisTitle: plot.xScale.title, }); - if (plot.title) { - context.save(); - context.textAlign = "center"; - context.textBaseline = "top"; - context.fillStyle = "black"; - context.font = "bold 12px sans-serif"; - context.fillText(plot.title, width / 2, 4); - context.restore(); - } - if (isMulti) { const radius = 5; for (let i = 0; i < shapes.length; i++) { context.save(); - context.translate(padding.left, titleHeight + legendItemHeight * i); + context.translate(padding.left, legendItemHeight * i); context.fillStyle = getColor(i); drawCircle({ context, @@ -266,7 +255,6 @@ const InnerDistributionsChart: FC<{ [ height, legendHeight, - titleHeight, samplesFooterHeight, shapes, samples, @@ -407,6 +395,7 @@ export const DistributionsChart: FC = ({ return (
+ {plot.title && } {plot.xScale.tag === "log" && shapes.value.some(hasMassBelowZero) ? ( Cannot graph distribution with negative values on logarithmic scale. diff --git a/packages/components/src/components/DynamicSquiggleViewer.tsx b/packages/components/src/components/DynamicSquiggleViewer.tsx index 6a9f46f648..5ee3209162 100644 --- a/packages/components/src/components/DynamicSquiggleViewer.tsx +++ b/packages/components/src/components/DynamicSquiggleViewer.tsx @@ -6,6 +6,7 @@ import { getResultVariables, getResultValue } from "../lib/utility.js"; import { CodeEditorHandle } from "./CodeEditor.js"; import { PartialPlaygroundSettings } from "./PlaygroundSettings.js"; import { SquiggleViewerHandle } from "./SquiggleViewer/index.js"; +import { ErrorBoundary } from "./ErrorBoundary.js"; type Props = { squiggleOutput: SquiggleOutput | undefined; @@ -34,14 +35,16 @@ export const DynamicSquiggleViewer = forwardRef( // `opacity-0 squiggle-semi-appear` would be better, but won't work reliably until we move Squiggle evaluation to Web Workers
)} - + + +
) : null; diff --git a/packages/components/src/components/ErrorBoundary.tsx b/packages/components/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000..21b8014532 --- /dev/null +++ b/packages/components/src/components/ErrorBoundary.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Component, PropsWithChildren } from "react"; + +type State = { + error?: Error; +}; + +export class ErrorBoundary extends Component { + public state: State = {}; + + public static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch() {} + + public render() { + if (this.state.error) { + return ( +
+
Fatal Error
+
{this.state.error.message}
+
Try reloading the browser.
+
{this.state.error.stack}
+
+ ); + } + + return this.props.children; + } +} diff --git a/packages/components/src/components/FunctionChart/DistFunctionChart.tsx b/packages/components/src/components/FunctionChart/DistFunctionChart.tsx index f4d5e52279..90904bbf47 100644 --- a/packages/components/src/components/FunctionChart/DistFunctionChart.tsx +++ b/packages/components/src/components/FunctionChart/DistFunctionChart.tsx @@ -28,6 +28,7 @@ import { DistributionsChart } from "../DistributionsChart/index.js"; import { ImageErrors } from "./ImageErrors.js"; import { getFunctionImage } from "./utils.js"; import { TailwindContext } from "@quri/ui"; +import { PlotTitle } from "../PlotTitle.js"; type FunctionChart1DistProps = { plot: SqDistFnPlot; @@ -150,6 +151,9 @@ function useDrawDistFunctionChart({ height, context, xTickFormat: plot.xScale.tickFormat, + yTickFormat: plot.yScale.tickFormat, + xAxisTitle: plot.xScale.title, + yAxisTitle: plot.yScale.title, }); d3ref.current = { frame, xScale }; @@ -303,6 +307,7 @@ export const DistFunctionChart: FC = ({ return (
+ {plot.title && }
Chart for {plot.toString()} diff --git a/packages/components/src/components/FunctionChart/NumericFunctionChart.tsx b/packages/components/src/components/FunctionChart/NumericFunctionChart.tsx index 240b01f10f..4b135da790 100644 --- a/packages/components/src/components/FunctionChart/NumericFunctionChart.tsx +++ b/packages/components/src/components/FunctionChart/NumericFunctionChart.tsx @@ -17,6 +17,7 @@ import { import { canvasClasses } from "../../lib/utility.js"; import { ImageErrors } from "./ImageErrors.js"; import { getFunctionImage } from "./utils.js"; +import { PlotTitle } from "../PlotTitle.js"; type Props = { plot: SqNumericFnPlot; @@ -60,6 +61,8 @@ export const NumericFunctionChart: FC = ({ yScale, xTickFormat: plot.xScale.tickFormat, yTickFormat: plot.yScale.tickFormat, + xAxisTitle: plot.xScale.title, + yAxisTitle: plot.yScale.title, }); if ( @@ -119,6 +122,7 @@ export const NumericFunctionChart: FC = ({ return (
+ {plot.title && } Chart for {plot.toString()} diff --git a/packages/components/src/components/FunctionChart/index.tsx b/packages/components/src/components/FunctionChart/index.tsx index 5d2c78ab9c..846772f6cc 100644 --- a/packages/components/src/components/FunctionChart/index.tsx +++ b/packages/components/src/components/FunctionChart/index.tsx @@ -17,6 +17,7 @@ import { import { SquiggleErrorAlert } from "../SquiggleErrorAlert.js"; import { DistFunctionChart } from "./DistFunctionChart.js"; import { NumericFunctionChart } from "./NumericFunctionChart.js"; +import { ErrorBoundary } from "../ErrorBoundary.js"; type FunctionChartProps = { fn: SqLambda; @@ -113,11 +114,13 @@ export const FunctionChart: FC = ({ }); return ( - + + + ); } default: diff --git a/packages/components/src/components/PlotTitle.tsx b/packages/components/src/components/PlotTitle.tsx new file mode 100644 index 0000000000..2ca6feb1fe --- /dev/null +++ b/packages/components/src/components/PlotTitle.tsx @@ -0,0 +1,9 @@ +import { FC } from "react"; + +export const PlotTitle: FC<{ title: string }> = ({ title }) => { + return ( +
+ {title} +
+ ); +}; diff --git a/packages/components/src/components/RelativeValuesGridChart/index.tsx b/packages/components/src/components/RelativeValuesGridChart/index.tsx index 5c06f63037..1fabc34fec 100644 --- a/packages/components/src/components/RelativeValuesGridChart/index.tsx +++ b/packages/components/src/components/RelativeValuesGridChart/index.tsx @@ -9,6 +9,7 @@ import { SqLambdaValue } from "@quri/squiggle-lang"; import { SqStringValue } from "@quri/squiggle-lang"; import { ErrorAlert } from "../Alert.js"; import { Env } from "@quri/squiggle-lang"; +import { PlotTitle } from "../PlotTitle.js"; const rvSchema = z.object({ median: z.number(), @@ -99,34 +100,41 @@ export const RelativeValuesGridChart: FC = ({ plot, environment }) => { const wrapFn = wrapFnResult.value.value; return ( -
-
- {ids.map((columnId) => ( - -
- - ))} - {ids.map((rowId) => ( - - -
+
+ {plot.title && ( +
+ +
+ )} +
+
+ {ids.map((columnId) => ( + +
- {ids.map((columnId) => ( - - ))} - - ))} + ))} + {ids.map((rowId) => ( + + +
+ + {ids.map((columnId) => ( + + ))} + + ))} +
); }; diff --git a/packages/components/src/components/ScatterChart/index.tsx b/packages/components/src/components/ScatterChart/index.tsx index bfef55c6d0..3857647a10 100644 --- a/packages/components/src/components/ScatterChart/index.tsx +++ b/packages/components/src/components/ScatterChart/index.tsx @@ -17,6 +17,7 @@ import { import { canvasClasses } from "../../lib/utility.js"; import { ErrorAlert } from "../Alert.js"; import { sqScaleToD3 } from "../../lib/d3/index.js"; +import { PlotTitle } from "../PlotTitle.js"; type Props = { plot: SqScatterPlot; @@ -70,6 +71,8 @@ export const ScatterChart: FC = ({ plot, height, environment }) => { yScale, xTickFormat: plot.xScale?.tickFormat, yTickFormat: plot.yScale?.tickFormat, + xAxisTitle: plot.xScale?.title, + yAxisTitle: plot.yScale?.title, drawTicks: true, }); @@ -106,6 +109,7 @@ export const ScatterChart: FC = ({ plot, height, environment }) => { return (
+ {plot.title && } Chart for {plot.toString()} diff --git a/packages/components/src/components/SquiggleViewer/VariableBox.tsx b/packages/components/src/components/SquiggleViewer/VariableBox.tsx index f3fed5ab81..adfba5769e 100644 --- a/packages/components/src/components/SquiggleViewer/VariableBox.tsx +++ b/packages/components/src/components/SquiggleViewer/VariableBox.tsx @@ -25,6 +25,7 @@ import { pathToShortName, } from "./utils.js"; import { useEffectRef } from "../../lib/hooks/useEffectRef.js"; +import { ErrorBoundary } from "../ErrorBoundary.js"; type SettingsMenuParams = { // Used to notify VariableBox that settings have changed, so that VariableBox could re-render itself. @@ -234,40 +235,42 @@ export const VariableBox: FC = ({ ); return ( -
- {(name !== undefined || isRoot) && ( -
-
- {!isFocused && triangleToggle()} - {headerName} - {!isFocused && headerPreview()} - {!isFocused && !isOpen && commentIcon()} - {!isRoot && editor && headerFindInEditorButton()} -
-
- {isOpen && headerString()} - {isOpen && headerSettingsButton()} -
-
- )} - {isOpen && ( -
- {!isFocused && isDictOrList && leftCollapseBorder()} - {!isFocused && !isDictOrList && !isRoot && ( -
// min-w-1rem = w-4 - )} -
- {commentPosition === "top" && hasComment && showComment()} - {children(getAdjustedMergedSettings(path))} - {commentPosition === "bottom" && hasComment && showComment()} + +
+ {(name !== undefined || isRoot) && ( +
+
+ {!isFocused && triangleToggle()} + {headerName} + {!isFocused && headerPreview()} + {!isFocused && !isOpen && commentIcon()} + {!isRoot && editor && headerFindInEditorButton()} +
+
+ {isOpen && headerString()} + {isOpen && headerSettingsButton()} +
+
+ )} + {isOpen && ( +
+ {!isFocused && isDictOrList && leftCollapseBorder()} + {!isFocused && !isDictOrList && !isRoot && ( +
// min-w-1rem = w-4 + )} +
+ {commentPosition === "top" && hasComment && showComment()} + {children(getAdjustedMergedSettings(path))} + {commentPosition === "bottom" && hasComment && showComment()} +
-
- )} -
+ )} +
+ ); }; diff --git a/packages/components/src/lib/draw/index.ts b/packages/components/src/lib/draw/index.ts index c90c4aa083..3de8f80eea 100644 --- a/packages/components/src/lib/draw/index.ts +++ b/packages/components/src/lib/draw/index.ts @@ -8,6 +8,8 @@ export const labelColor = "rgb(114, 125, 147)"; export const cursorLineColor = "#888"; export const primaryColor = "#4c78a8"; // for lines and areas export const distributionColor = "#6d9bce"; // for distributions. Slightly lighter than primaryColor +export const axisTitleColor = "rgb(100 116 139)"; +export const axisTitleFont = "bold 12px ui-sans-serif, system-ui"; const labelFont = "10px sans-serif"; const xLabelOffset = 6; const yLabelOffset = 6; @@ -31,6 +33,8 @@ interface DrawAxesParams { yTickCount?: number; xTickFormat?: string; yTickFormat?: string; + xAxisTitle?: string; + yAxisTitle?: string; } export function drawAxes({ @@ -46,6 +50,8 @@ export function drawAxes({ yTickCount = Math.max(Math.min(Math.floor(height / 100), 12), 3), xTickFormat: xTickFormatSpecifier = defaultTickFormatSpecifier, yTickFormat: yTickFormatSpecifier = defaultTickFormatSpecifier, + xAxisTitle, + yAxisTitle, }: DrawAxesParams) { const xTicks = xScale.ticks(xTickCount); const xTickFormat = xScale.tickFormat(xTickCount, xTickFormatSpecifier); @@ -56,6 +62,12 @@ export function drawAxes({ const tickSize = 2; const padding: Padding = { ...suggestedPadding }; + if (xAxisTitle) { + padding.bottom = padding.bottom + 20; + } + if (yAxisTitle) { + padding.left = padding.left + 35; + } // measure tick sizes for dynamic padding if (!hideYAxis) { @@ -186,6 +198,31 @@ export function drawAxes({ frame.exit(); } + if (xAxisTitle) { + const chartWidth = width - padding.left - padding.right; // Actual charting area width + const titleX = padding.left + chartWidth / 2; // center the title within the charting area + const titleY = height - padding.bottom + 33; // adjust this value based on desired distance from x-axis + context.textAlign = "center"; + context.textBaseline = "bottom"; + context.font = axisTitleFont; + context.fillStyle = axisTitleColor; + context.fillText(xAxisTitle, titleX, titleY); + } + if (yAxisTitle) { + const chartHeight = height - padding.top - padding.bottom; // Actual charting area height + const titleY = padding.top + chartHeight / 2; // center the title vertically within the charting area + const titleX = 0; + context.save(); // save the current context state + context.translate(titleX, titleY); + context.rotate(-Math.PI / 2); // rotate 90 degrees counter-clockwise + context.textAlign = "center"; + context.textBaseline = "top"; + context.font = axisTitleFont; + context.fillStyle = axisTitleColor; + context.fillText(yAxisTitle, 0, 0); + context.restore(); // restore the context state to before rotation and translation + } + return { xScale, yScale, diff --git a/packages/components/src/stories/SquiggleChart/Distributions.stories.tsx b/packages/components/src/stories/SquiggleChart/Distributions.stories.tsx index 4609121bca..d0e8c3a51d 100644 --- a/packages/components/src/stories/SquiggleChart/Distributions.stories.tsx +++ b/packages/components/src/stories/SquiggleChart/Distributions.stories.tsx @@ -15,7 +15,7 @@ type Story = StoryObj; export const ContinuousSymbolic: Story = { name: "Continuous Symbolic", args: { - code: "normal(5,2)", + code: "Sym.normal(5,2)", }, }; @@ -53,7 +53,7 @@ export const ContinuousSampleSet1MSamples: Story = { export const Discrete: Story = { args: { - code: "mx(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])", + code: "mx([0, 1, 3, 5, 8, 10], [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])", }, }; @@ -61,19 +61,31 @@ export const Scales: Story = { name: "Continuous with scales", args: { code: `Plot.dist({ - dist: -1 to 5, + dist: 1 to 5, xScale: Scale.symlog(), yScale: Scale.power({ exponent: 0.1 }), })`, }, }; +export const SymbolicWithXLabel: Story = { + name: "Symbolic with x label", + args: { + code: `Plot.dist({ + dist: Sym.normal(5,2), + xScale: Scale.linear({title: "X Scale"}), + yScale: Scale.linear(), +})`, + }, +}; + export const CustomTickFormat: Story = { name: "Custom tick format", args: { code: `Plot.dist({ dist: beta(3, 5), - xScale: Scale.linear({ tickFormat: ".0%" }), + title: "Beta(3, 5)", + xScale: Scale.linear({ tickFormat: ".0%" , title: "X Scale"}), })`, }, }; @@ -81,7 +93,7 @@ export const CustomTickFormat: Story = { export const Mixed: Story = { name: "Mixed", args: { - code: "mx(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])", + code: "mx([0, 1, 3, 5, 8, normal(8, 1)], [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])", }, }; @@ -90,6 +102,8 @@ export const MultiplePlots: Story = { args: { code: ` Plot.dists({ + title: "Multiple plots", + xScale: Scale.linear({ title: "X Scale" }), dists: [ { name: "one", diff --git a/packages/components/src/stories/SquiggleChart/Functions.stories.tsx b/packages/components/src/stories/SquiggleChart/Functions.stories.tsx index cb1c277ccf..78a9449afb 100644 --- a/packages/components/src/stories/SquiggleChart/Functions.stories.tsx +++ b/packages/components/src/stories/SquiggleChart/Functions.stories.tsx @@ -35,9 +35,16 @@ export const LogScale: Story = { foo(t) = t^2 Plot.numericFn({ fn: foo, + title: "My Plot's Title", xScale: Scale.log({ min: 1, - max: 100 + max: 100, + title: "x Axis Title" + }), + yScale: Scale.linear({ + min: 1, + max: 10000, + title: "y Axis Title" }) }) `, @@ -50,6 +57,7 @@ export const FairLogScaleSampling: Story = { code: sq` numericPlot = Plot.numericFn({ fn: {|t| t < 5 ? 1000 : t^2}, + title: "Fair Long Scale Sampling Title", xScale: Scale.log({ min: 1, max: 100 diff --git a/packages/components/src/stories/SquiggleChart/ScatterPlot.stories.tsx b/packages/components/src/stories/SquiggleChart/ScatterPlot.stories.tsx index 699b5b974a..aa023194e0 100644 --- a/packages/components/src/stories/SquiggleChart/ScatterPlot.stories.tsx +++ b/packages/components/src/stories/SquiggleChart/ScatterPlot.stories.tsx @@ -41,13 +41,14 @@ Plot.scatter({ export const DoubleSymlog: Story = { args: { code: ` -xDist = SampleSet.fromDist(-2 to 5) +xDist = SampleSet.fromDist(1 to 5) yDist = normal(0, 10) * 5 - xDist Plot.scatter({ + title: "Double Symlog Scatter Plot", xDist: xDist, yDist: yDist, - xScale: Scale.symlog(), - yScale: Scale.symlog(), + xScale: Scale.symlog({title: "X Scale"}), + yScale: Scale.symlog({title: "Y Scale"}), }) `, }, diff --git a/packages/components/src/stories/SquigglePlayground.stories.tsx b/packages/components/src/stories/SquigglePlayground.stories.tsx index a30765e8fe..0b490ea773 100644 --- a/packages/components/src/stories/SquigglePlayground.stories.tsx +++ b/packages/components/src/stories/SquigglePlayground.stories.tsx @@ -75,6 +75,7 @@ fn = { |id1, id2| } RelativeValues.gridPlot({ + title: "My Relative Values Plot", ids: ids, fn: fn }) @@ -230,16 +231,6 @@ bar = 123 }, }; -export const Failure: Story = { - name: "Failure", - args: { - defaultCode: `Table.make( - { data:[], columns: [{ name: "Features", fn: {|r|""} }] } - )`, - height: 800, - }, -}; - export const Calculator: Story = { name: "Calculator", args: { diff --git a/packages/components/test/d3.test.ts b/packages/components/test/d3.test.ts index f348376512..683b5cf73c 100644 --- a/packages/components/test/d3.test.ts +++ b/packages/components/test/d3.test.ts @@ -38,7 +38,7 @@ describe.each([ if (num !== 0 && !(sqScale instanceof SqLogScale)) { test("negative", () => { - expect(format(-num)).toEqual("-" + result); + expect(format(-num)).toEqual(result === "0" ? result : "-" + result); }); } }); diff --git a/packages/squiggle-lang/__tests__/library/scale_test.ts b/packages/squiggle-lang/__tests__/library/scale_test.ts index 8bdf409b0b..01e9da0340 100644 --- a/packages/squiggle-lang/__tests__/library/scale_test.ts +++ b/packages/squiggle-lang/__tests__/library/scale_test.ts @@ -6,6 +6,10 @@ describe("Scales", () => { "Scale.linear({ min: 10, max: 5 })", "Max must be greater than min, got: min=10, max=5" ); + testEvalToMatch( + "Scale.linear({ min: 5, max: 10, tickFormat: '....' })", + "Error(Argument Error: Tick format [....] is invalid.)" + ); testEvalToBe("Scale.log()", "Logarithmic scale"); testEvalToMatch( diff --git a/packages/squiggle-lang/src/fr/builtin.ts b/packages/squiggle-lang/src/fr/builtin.ts index ea5db0afee..1d90294f0c 100644 --- a/packages/squiggle-lang/src/fr/builtin.ts +++ b/packages/squiggle-lang/src/fr/builtin.ts @@ -91,7 +91,7 @@ export const library = [ name: "inspect", definitions: [ makeDefinition([frAny], ([value]) => { - console.log(value.toString()); + console.log(value); return value; }), makeDefinition([frAny, frString], ([value, label]) => { diff --git a/packages/squiggle-lang/src/fr/plot.ts b/packages/squiggle-lang/src/fr/plot.ts index bf6d62d730..410aacc044 100644 --- a/packages/squiggle-lang/src/fr/plot.ts +++ b/packages/squiggle-lang/src/fr/plot.ts @@ -183,10 +183,11 @@ export const library = [ ["fn", frLambda], ["xScale", frOptional(frScale)], ["yScale", frOptional(frScale)], + ["title", frOptional(frString)], ["points", frOptional(frNumber)] ), ], - ([{ fn, xScale, yScale, points }]) => { + ([{ fn, xScale, yScale, title, points }]) => { const domain = extractDomainFromOneArgFunction(fn); return vPlot({ type: "numericFn", @@ -194,6 +195,7 @@ export const library = [ xScale: createScale(xScale, domain), yScale: yScale ?? defaultScale, points: points ?? undefined, + title: title ?? undefined, }); } ), @@ -213,10 +215,11 @@ export const library = [ ["xScale", frOptional(frScale)], ["yScale", frOptional(frScale)], ["distXScale", frOptional(frScale)], + ["title", frOptional(frString)], ["points", frOptional(frNumber)] ), ], - ([{ fn, xScale, yScale, distXScale, points }]) => { + ([{ fn, xScale, yScale, distXScale, title, points }]) => { const domain = extractDomainFromOneArgFunction(fn); return vPlot({ type: "distFn", @@ -224,6 +227,7 @@ export const library = [ xScale: createScale(xScale, domain), yScale: yScale ?? defaultScale, distXScale: distXScale ?? yScale ?? defaultScale, + title: title ?? undefined, points: points ?? undefined, }); } @@ -244,16 +248,18 @@ export const library = [ ["xDist", frDist], ["yDist", frDist], ["xScale", frOptional(frScale)], - ["yScale", frOptional(frScale)] + ["yScale", frOptional(frScale)], + ["title", frOptional(frString)] ), ], - ([{ xDist, yDist, xScale, yScale }]) => { + ([{ xDist, yDist, xScale, yScale, title }]) => { return vPlot({ type: "scatter", xDist, yDist, xScale: xScale ?? defaultScale, yScale: yScale ?? defaultScale, + title: title ?? undefined, }); } ), diff --git a/packages/squiggle-lang/src/fr/relativeValues.ts b/packages/squiggle-lang/src/fr/relativeValues.ts index 0e6c178d10..0ab7a52f60 100644 --- a/packages/squiggle-lang/src/fr/relativeValues.ts +++ b/packages/squiggle-lang/src/fr/relativeValues.ts @@ -5,6 +5,7 @@ import { frLambda, frDict, frString, + frOptional, } from "../library/registry/frTypes.js"; import { FnFactory } from "../library/registry/helpers.js"; import { makeSquiggleDefinition } from "../library/registry/squiggleDefinition.js"; @@ -18,7 +19,8 @@ const maker = new FnFactory({ const relativeValuesShape = frDict( ["ids", frArray(frString)], - ["fn", frLambda] + ["fn", frLambda], + ["title", frOptional(frString)] ); export const library = [ @@ -32,11 +34,12 @@ export const library = [ })`, ], definitions: [ - makeDefinition([relativeValuesShape], ([{ ids, fn }]) => { + makeDefinition([relativeValuesShape], ([{ ids, fn, title }]) => { return vPlot({ type: "relativeValues", fn, ids, + title: title ?? undefined, }); }), ], diff --git a/packages/squiggle-lang/src/fr/scale.ts b/packages/squiggle-lang/src/fr/scale.ts index bcee2c1da5..e824dbdb7a 100644 --- a/packages/squiggle-lang/src/fr/scale.ts +++ b/packages/squiggle-lang/src/fr/scale.ts @@ -1,4 +1,4 @@ -import { REOther } from "../errors/messages.js"; +import { REArgumentError, REOther } from "../errors/messages.js"; import { makeDefinition } from "../library/registry/fnDefinition.js"; import { frDict, @@ -17,31 +17,44 @@ const maker = new FnFactory({ const commonDict = frDict( ["min", frOptional(frNumber)], ["max", frOptional(frNumber)], - ["tickFormat", frOptional(frString)] + ["tickFormat", frOptional(frString)], + ["title", frOptional(frString)] ); function checkMinMax(min: number | null, max: number | null) { if (min !== null && max !== null && max <= min) { - throw new REOther( + throw new REArgumentError( `Max must be greater than min, got: min=${min}, max=${max}` ); } } +// Regex taken from d3-format. +// https://github.com/d3/d3-format/blob/f3cb31091df80a08f25afd4a7af2dcb3a6cd5eef/src/formatSpecifier.js#L1C65-L2C85 +const d3TickFormatRegex = + /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i; + +function checkTickFormat(tickFormat: string | null) { + if (tickFormat && !d3TickFormatRegex.test(tickFormat)) { + throw new REArgumentError(`Tick format [${tickFormat}] is invalid.`); + } +} + export const library = [ maker.make({ name: "linear", output: "Scale", examples: [`Scale.linear({ min: 3, max: 10 })`], definitions: [ - makeDefinition([commonDict], ([{ min, max, tickFormat }]) => { + makeDefinition([commonDict], ([{ min, max, tickFormat, title }]) => { checkMinMax(min, max); - + checkTickFormat(tickFormat); return vScale({ type: "linear", min: min ?? undefined, max: max ?? undefined, tickFormat: tickFormat ?? undefined, + title: title ?? undefined, }); }), makeDefinition([], () => { @@ -54,27 +67,20 @@ export const library = [ output: "Scale", examples: [`Scale.log({ min: 1, max: 100 })`], definitions: [ - makeDefinition( - [ - frDict( - ["min", frOptional(frNumber)], - ["max", frOptional(frNumber)], - ["tickFormat", frOptional(frString)] - ), - ], - ([{ min, max, tickFormat }]) => { - if (min !== null && min <= 0) { - throw new REOther(`Min must be over 0 for log scale, got: ${min}`); - } - checkMinMax(min, max); - return vScale({ - type: "log", - min: min ?? undefined, - max: max ?? undefined, - tickFormat: tickFormat ?? undefined, - }); + makeDefinition([commonDict], ([{ min, max, tickFormat, title }]) => { + if (min !== null && min <= 0) { + throw new REOther(`Min must be over 0 for log scale, got: ${min}`); } - ), + checkMinMax(min, max); + checkTickFormat(tickFormat); + return vScale({ + type: "log", + min: min ?? undefined, + max: max ?? undefined, + tickFormat: tickFormat ?? undefined, + title: title ?? undefined, + }); + }), makeDefinition([], () => { return vScale({ type: "log" }); }), @@ -91,11 +97,13 @@ export const library = [ ["min", frOptional(frNumber)], ["max", frOptional(frNumber)], ["tickFormat", frOptional(frString)], + ["title", frOptional(frString)], ["constant", frOptional(frNumber)] ), ], - ([{ min, max, tickFormat, constant }]) => { + ([{ min, max, tickFormat, title, constant }]) => { checkMinMax(min, max); + checkTickFormat(tickFormat); if (constant !== null && constant === 0) { throw new REOther(`Symlog scale constant cannot be 0.`); } @@ -106,6 +114,7 @@ export const library = [ max: max ?? undefined, tickFormat: tickFormat ?? undefined, constant: constant ?? undefined, + title: title ?? undefined, }); } ), @@ -127,11 +136,13 @@ export const library = [ ["min", frOptional(frNumber)], ["max", frOptional(frNumber)], ["tickFormat", frOptional(frString)], + ["title", frOptional(frString)], ["exponent", frOptional(frNumber)] ), ], - ([{ min, max, tickFormat, exponent }]) => { + ([{ min, max, tickFormat, title, exponent }]) => { checkMinMax(min, max); + checkTickFormat(tickFormat); if (exponent !== null && exponent <= 0) { throw new REOther(`Power Scale exponent must be over 0.`); } @@ -142,6 +153,7 @@ export const library = [ max: max ?? undefined, tickFormat: tickFormat ?? undefined, exponent: exponent ?? undefined, + title: title ?? undefined, }); } ), diff --git a/packages/squiggle-lang/src/library/registry/frTypes.ts b/packages/squiggle-lang/src/library/registry/frTypes.ts index 06a110b3df..b65a7a43fc 100644 --- a/packages/squiggle-lang/src/library/registry/frTypes.ts +++ b/packages/squiggle-lang/src/library/registry/frTypes.ts @@ -266,6 +266,31 @@ export function frDict< [k in K4]: T4; } & { [k in K5]: T5 } >; +export function frDict< + K1 extends string, + T1, + K2 extends string, + T2, + K3 extends string, + T3, + K4 extends string, + T4, + K5 extends string, + T5, + K6 extends string, + T6, +>( + kv1: [K1, FRType], + kv2: [K2, FRType], + kv3: [K3, FRType], + kv4: [K4, FRType], + kv5: [K5, FRType], + kv6: [K6, FRType] +): FRType< + { [k in K1]: T1 } & { [k in K2]: T2 } & { [k in K3]: T3 } & { + [k in K4]: T4; + } & { [k in K5]: T5 } & { [k in K6]: T6 } +>; export function frDict( ...allKvs: [string, FRType][] diff --git a/packages/squiggle-lang/src/public/SqValue/SqPlot.ts b/packages/squiggle-lang/src/public/SqValue/SqPlot.ts index 2abaeb56de..4bdb28ab35 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqPlot.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqPlot.ts @@ -47,9 +47,13 @@ abstract class SqAbstractPlot { return vPlot(this._value).toString(); } - asValue() { + get asValue() { return new SqPlotValue(vPlot(this._value), this.context); } + + get title(): string | undefined { + return this._value.title; + } } export class SqDistributionsPlot extends SqAbstractPlot<"distributions"> { @@ -85,10 +89,6 @@ export class SqDistributionsPlot extends SqAbstractPlot<"distributions"> { })); } - get title(): string | undefined { - return this._value.title; - } - get showSummary(): boolean { return this._value.showSummary; } @@ -113,11 +113,13 @@ export class SqNumericFnPlot extends SqAbstractPlot<"numericFn"> { xScale, yScale, points, + title, }: { fn: SqLambda; xScale: SqScale; yScale: SqScale; points?: number; + title?: string; }) { const result = new SqNumericFnPlot( { @@ -125,6 +127,7 @@ export class SqNumericFnPlot extends SqAbstractPlot<"numericFn"> { fn: fn._value, xScale: xScale._value, yScale: yScale._value, + title: title, points, }, fn.context @@ -171,12 +174,14 @@ export class SqDistFnPlot extends SqAbstractPlot<"distFn"> { xScale, yScale, distXScale, + title, points, }: { fn: SqLambda; xScale: SqScale; yScale: SqScale; distXScale: SqScale; + title?: string; points?: number; }) { const result = new SqDistFnPlot( @@ -186,6 +191,7 @@ export class SqDistFnPlot extends SqAbstractPlot<"distFn"> { xScale: xScale._value, yScale: yScale._value, distXScale: distXScale._value, + title: title, points, }, fn.context diff --git a/packages/squiggle-lang/src/public/SqValue/SqScale.ts b/packages/squiggle-lang/src/public/SqValue/SqScale.ts index 772fd40e97..f0b1bb8325 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqScale.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqScale.ts @@ -40,6 +40,9 @@ abstract class SqAbstractScale { get tickFormat() { return this._value.tickFormat; } + get title() { + return this._value.title; + } } export class SqLinearScale extends SqAbstractScale<"linear"> { diff --git a/packages/squiggle-lang/src/value/index.ts b/packages/squiggle-lang/src/value/index.ts index d25dd2ed12..0832bdf8de 100644 --- a/packages/squiggle-lang/src/value/index.ts +++ b/packages/squiggle-lang/src/value/index.ts @@ -286,6 +286,7 @@ export type CommonScaleArgs = { min?: number; max?: number; tickFormat?: string; + title?: string; }; export type Scale = CommonScaleArgs & @@ -372,42 +373,47 @@ export type LabeledDistribution = { distribution: BaseDist; }; -export type Plot = - | { - type: "distributions"; - distributions: LabeledDistribution[]; - xScale: Scale; - yScale: Scale; - title?: string; - showSummary: boolean; - } - | { - type: "numericFn"; - fn: Lambda; - xScale: Scale; - yScale: Scale; - points?: number; - } - | { - type: "distFn"; - fn: Lambda; - xScale: Scale; - yScale: Scale; - distXScale: Scale; - points?: number; - } - | { - type: "scatter"; - xDist: BaseDist; - yDist: BaseDist; - xScale: Scale; - yScale: Scale; - } - | { - type: "relativeValues"; - fn: Lambda; - ids: string[]; - }; +export type CommonPlotArgs = { + title?: string; +}; + +export type Plot = CommonPlotArgs & + ( + | { + type: "distributions"; + distributions: LabeledDistribution[]; + xScale: Scale; + yScale: Scale; + showSummary: boolean; + } + | { + type: "numericFn"; + fn: Lambda; + xScale: Scale; + yScale: Scale; + points?: number; + } + | { + type: "distFn"; + fn: Lambda; + xScale: Scale; + yScale: Scale; + distXScale: Scale; + points?: number; + } + | { + type: "scatter"; + xDist: BaseDist; + yDist: BaseDist; + xScale: Scale; + yScale: Scale; + } + | { + type: "relativeValues"; + fn: Lambda; + ids: string[]; + } + ); export type TableChart = { data: Value[]; diff --git a/packages/website/src/pages/docs/Api/Plot.mdx b/packages/website/src/pages/docs/Api/Plot.mdx index 16a4dd6854..db5a7fd25c 100644 --- a/packages/website/src/pages/docs/Api/Plot.mdx +++ b/packages/website/src/pages/docs/Api/Plot.mdx @@ -58,7 +58,7 @@ Examples: number), xScale: scale, yScale: scale, + title: string, points: number }) => plot ``` @@ -97,6 +98,7 @@ Plot.distFn: ({ fn: (number => dist), xScale: scale, yScale: scale, + title: string, distXScale: scale, points: number }) => plot @@ -105,7 +107,9 @@ Plot.distFn: ({ @@ -115,11 +119,12 @@ Plot.distFn: ({ Plots a scatterplot. Requires two sample set distributions. ``` -Plot.numericFn: ({ +Plot.scatter: ({ yDist: sampleSetDist, xDist: sampleSetDist, xScale: Scale, - yScale: Scale + yScale: Scale, + title: string, }) => plot ``` @@ -137,10 +142,11 @@ Plot.scatter({ defaultCode={`xDist = SampleSet.fromDist(normal({p5:-2, p95:5})) yDist = normal({p5:-3, p95:3}) * 5 - xDist Plot.scatter({ + title: "A Scatterplot", xDist: xDist, yDist: yDist, - xScale: Scale.symlog(), - yScale: Scale.symlog(), + xScale: Scale.symlog({title: "X Axis Title"}), + yScale: Scale.symlog({title: "Y Axis Title"}), })`} /> @@ -154,19 +160,22 @@ We use D3 for the tick formats. You can read about custom tick formats [here](ht Scale.log: ({ min: number, max: number, - tickFormat: string + tickFormat: string, + title: string }) => scale Scale.linear: ({ min: number, max: number, - tickFormat: string + tickFormat: string, + title: string }) => scale Scale.symlog: ({ min: number, max: number, tickFormat: string, + title: string, constant: number }) => scale @@ -174,6 +183,7 @@ Scale.power: ({ min: number, max: number, tickFormat: string, + title: string, exponent: number }) => scale ```