modal window for local settings
This commit is contained in:
		
							parent
							
								
									8f4259cef3
								
							
						
					
					
						commit
						eefdfbb2fe
					
				|  | @ -8,26 +8,24 @@ import { | ||||||
| import { Vega } from "react-vega"; | import { Vega } from "react-vega"; | ||||||
| import { ErrorAlert } from "./Alert"; | import { ErrorAlert } from "./Alert"; | ||||||
| import { useSize } from "react-use"; | import { useSize } from "react-use"; | ||||||
| import clsx from "clsx"; |  | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   buildVegaSpec, |   buildVegaSpec, | ||||||
|   DistributionChartSpecOptions, |   DistributionChartSpecOptions, | ||||||
| } from "../lib/distributionSpecBuilder"; | } from "../lib/distributionSpecBuilder"; | ||||||
| import { NumberShower } from "./NumberShower"; | import { NumberShower } from "./NumberShower"; | ||||||
|  | import { hasMassBelowZero } from "../lib/distributionUtils"; | ||||||
| 
 | 
 | ||||||
| export type DistributionPlottingSettings = { | export type DistributionPlottingSettings = { | ||||||
|   /** Whether to show a summary of means, stdev, percentiles etc */ |   /** Whether to show a summary of means, stdev, percentiles etc */ | ||||||
|   showSummary: boolean; |   showSummary: boolean; | ||||||
|   /** Whether to show the user graph controls (scale etc) */ |   actions?: boolean; | ||||||
|   showControls: boolean; |  | ||||||
| } & DistributionChartSpecOptions; | } & DistributionChartSpecOptions; | ||||||
| 
 | 
 | ||||||
| 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> = (props) => { | export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | ||||||
|  | @ -36,17 +34,9 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | ||||||
|     height, |     height, | ||||||
|     showSummary, |     showSummary, | ||||||
|     width, |     width, | ||||||
|     showControls, |  | ||||||
|     logX, |     logX, | ||||||
|     expY, |  | ||||||
|     actions = false, |     actions = false, | ||||||
|   } = props; |   } = props; | ||||||
|   const [isLogX, setLogX] = React.useState(logX); |  | ||||||
|   const [isExpY, setExpY] = React.useState(expY); |  | ||||||
| 
 |  | ||||||
|   React.useEffect(() => setLogX(logX), [logX]); |  | ||||||
|   React.useEffect(() => setExpY(expY), [expY]); |  | ||||||
| 
 |  | ||||||
|   const shape = distribution.pointSet(); |   const shape = distribution.pointSet(); | ||||||
|   const [sized] = useSize((size) => { |   const [sized] = useSize((size) => { | ||||||
|     if (shape.tag === "Error") { |     if (shape.tag === "Error") { | ||||||
|  | @ -57,9 +47,6 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const massBelow0 = |  | ||||||
|       shape.value.continuous.some((x) => x.x <= 0) || |  | ||||||
|       shape.value.discrete.some((x) => x.x <= 0); |  | ||||||
|     const spec = buildVegaSpec(props); |     const spec = buildVegaSpec(props); | ||||||
| 
 | 
 | ||||||
|     let widthProp = width ? width : size.width; |     let widthProp = width ? width : size.width; | ||||||
|  | @ -72,7 +59,11 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div style={{ width: widthProp }}> |       <div style={{ width: widthProp }}> | ||||||
|         {!(isLogX && massBelow0) ? ( |         {logX && hasMassBelowZero(shape.value) ? ( | ||||||
|  |           <ErrorAlert heading="Log Domain Error"> | ||||||
|  |             Cannot graph distribution with negative values on logarithmic scale. | ||||||
|  |           </ErrorAlert> | ||||||
|  |         ) : ( | ||||||
|           <Vega |           <Vega | ||||||
|             spec={spec} |             spec={spec} | ||||||
|             data={{ con: shape.value.continuous, dis: shape.value.discrete }} |             data={{ con: shape.value.continuous, dis: shape.value.discrete }} | ||||||
|  | @ -80,67 +71,16 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | ||||||
|             height={height} |             height={height} | ||||||
|             actions={actions} |             actions={actions} | ||||||
|           /> |           /> | ||||||
|         ) : ( |  | ||||||
|           <ErrorAlert heading="Log Domain Error"> |  | ||||||
|             Cannot graph distribution with negative values on logarithmic scale. |  | ||||||
|           </ErrorAlert> |  | ||||||
|         )} |         )} | ||||||
|         <div className="flex justify-center"> |         <div className="flex justify-center"> | ||||||
|           {showSummary && <SummaryTable distribution={distribution} />} |           {showSummary && <SummaryTable distribution={distribution} />} | ||||||
|         </div> |         </div> | ||||||
|         {showControls && ( |  | ||||||
|           <div> |  | ||||||
|             <CheckBox |  | ||||||
|               label="Log X scale" |  | ||||||
|               value={isLogX} |  | ||||||
|               onChange={setLogX} |  | ||||||
|               // Check whether we should disable the checkbox
 |  | ||||||
|               {...(massBelow0 |  | ||||||
|                 ? { |  | ||||||
|                     disabled: true, |  | ||||||
|                     tooltip: |  | ||||||
|                       "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.", |  | ||||||
|                   } |  | ||||||
|                 : {})} |  | ||||||
|             /> |  | ||||||
|             <CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} /> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
|   return sized; |   return sized; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| interface CheckBoxProps { |  | ||||||
|   label: string; |  | ||||||
|   onChange: (x: boolean) => void; |  | ||||||
|   value: boolean; |  | ||||||
|   disabled?: boolean; |  | ||||||
|   tooltip?: string; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const CheckBox: React.FC<CheckBoxProps> = ({ |  | ||||||
|   label, |  | ||||||
|   onChange, |  | ||||||
|   value, |  | ||||||
|   disabled = false, |  | ||||||
|   tooltip, |  | ||||||
| }) => { |  | ||||||
|   return ( |  | ||||||
|     <span title={tooltip}> |  | ||||||
|       <input |  | ||||||
|         type="checkbox" |  | ||||||
|         checked={value} |  | ||||||
|         onChange={() => onChange(!value)} |  | ||||||
|         disabled={disabled} |  | ||||||
|         className="form-checkbox" |  | ||||||
|       /> |  | ||||||
|       <label className={clsx(disabled && "text-slate-400")}> {label}</label> |  | ||||||
|     </span> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({ | const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({ | ||||||
|   children, |   children, | ||||||
| }) => ( | }) => ( | ||||||
|  |  | ||||||
|  | @ -35,8 +35,6 @@ export interface SquiggleChartProps { | ||||||
|   jsImports?: jsImports; |   jsImports?: jsImports; | ||||||
|   /** Whether to show a summary of the distribution */ |   /** Whether to show a summary of the distribution */ | ||||||
|   showSummary?: boolean; |   showSummary?: boolean; | ||||||
|   /** Whether to show graph controls (scale etc)*/ |  | ||||||
|   showControls?: boolean; |  | ||||||
|   /** Set the x scale to be logarithmic by deault */ |   /** Set the x scale to be logarithmic by deault */ | ||||||
|   logX?: boolean; |   logX?: boolean; | ||||||
|   /** Set the y scale to be exponential by deault */ |   /** Set the y scale to be exponential by deault */ | ||||||
|  | @ -67,7 +65,6 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo( | ||||||
|     jsImports = defaultImports, |     jsImports = defaultImports, | ||||||
|     showSummary = false, |     showSummary = false, | ||||||
|     width, |     width, | ||||||
|     showControls = false, |  | ||||||
|     logX = false, |     logX = false, | ||||||
|     expY = false, |     expY = false, | ||||||
|     diagramStart = 0, |     diagramStart = 0, | ||||||
|  | @ -89,7 +86,6 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo( | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const distributionPlotSettings = { |     const distributionPlotSettings = { | ||||||
|       showControls, |  | ||||||
|       showSummary, |       showSummary, | ||||||
|       logX, |       logX, | ||||||
|       expY, |       expY, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import React, { FC, useState, useEffect, useMemo } from "react"; | import React, { FC, useState, useEffect, useMemo } from "react"; | ||||||
| import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; | import { useForm, UseFormRegister, useWatch } from "react-hook-form"; | ||||||
| import * as yup from "yup"; | import * as yup from "yup"; | ||||||
| import { useMaybeControlledValue } from "../lib/hooks"; | import { useMaybeControlledValue } from "../lib/hooks"; | ||||||
| import { yupResolver } from "@hookform/resolvers/yup"; | import { yupResolver } from "@hookform/resolvers/yup"; | ||||||
|  | @ -24,8 +24,15 @@ import { JsonEditor } from "./JsonEditor"; | ||||||
| import { ErrorAlert, SuccessAlert } from "./Alert"; | import { ErrorAlert, SuccessAlert } from "./Alert"; | ||||||
| import { SquiggleContainer } from "./SquiggleContainer"; | import { SquiggleContainer } from "./SquiggleContainer"; | ||||||
| import { Toggle } from "./ui/Toggle"; | import { Toggle } from "./ui/Toggle"; | ||||||
| import { Checkbox } from "./ui/Checkbox"; |  | ||||||
| import { StyledTab } from "./ui/StyledTab"; | import { StyledTab } from "./ui/StyledTab"; | ||||||
|  | import { InputItem } from "./ui/InputItem"; | ||||||
|  | import { Text } from "./ui/Text"; | ||||||
|  | import { ViewSettings, viewSettingsSchema } from "./ViewSettings"; | ||||||
|  | import { HeadedSection } from "./ui/HeadedSection"; | ||||||
|  | import { | ||||||
|  |   defaultColor, | ||||||
|  |   defaultTickFormat, | ||||||
|  | } from "../lib/distributionSpecBuilder"; | ||||||
| 
 | 
 | ||||||
| type PlaygroundProps = SquiggleChartProps & { | type PlaygroundProps = SquiggleChartProps & { | ||||||
|   /** The initial squiggle string to put in the playground */ |   /** The initial squiggle string to put in the playground */ | ||||||
|  | @ -37,7 +44,9 @@ type PlaygroundProps = SquiggleChartProps & { | ||||||
|   showEditor?: boolean; |   showEditor?: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const schema = yup.object({}).shape({ | const schema = yup | ||||||
|  |   .object({}) | ||||||
|  |   .shape({ | ||||||
|     sampleCount: yup |     sampleCount: yup | ||||||
|       .number() |       .number() | ||||||
|       .required() |       .required() | ||||||
|  | @ -54,73 +63,11 @@ const schema = yup.object({}).shape({ | ||||||
|       .default(1000) |       .default(1000) | ||||||
|       .min(10) |       .min(10) | ||||||
|       .max(10000), |       .max(10000), | ||||||
|   chartHeight: yup.number().required().positive().integer().default(350), |   }) | ||||||
|   leftSizePercent: yup |   .concat(viewSettingsSchema); | ||||||
|     .number() |  | ||||||
|     .required() |  | ||||||
|     .positive() |  | ||||||
|     .integer() |  | ||||||
|     .min(10) |  | ||||||
|     .max(100) |  | ||||||
|     .default(50), |  | ||||||
|   showControls: yup.boolean().required(), |  | ||||||
|   showSummary: yup.boolean().required(), |  | ||||||
|   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), |  | ||||||
|   diagramCount: yup.number().required().positive().integer().default(20).min(2), |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| type FormFields = yup.InferType<typeof schema>; | type FormFields = yup.InferType<typeof schema>; | ||||||
| 
 | 
 | ||||||
| const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({ |  | ||||||
|   title, |  | ||||||
|   children, |  | ||||||
| }) => ( |  | ||||||
|   <div> |  | ||||||
|     <header className="text-lg leading-6 font-medium text-gray-900"> |  | ||||||
|       {title} |  | ||||||
|     </header> |  | ||||||
|     <div className="mt-4">{children}</div> |  | ||||||
|   </div> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const Text: FC<{ children: React.ReactNode }> = ({ children }) => ( |  | ||||||
|   <p className="text-sm text-gray-500">{children}</p> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| function InputItem<T>({ |  | ||||||
|   name, |  | ||||||
|   label, |  | ||||||
|   type, |  | ||||||
|   register, |  | ||||||
| }: { |  | ||||||
|   name: Path<T>; |  | ||||||
|   label: string; |  | ||||||
|   type: "number" | "text" | "color"; |  | ||||||
|   register: UseFormRegister<T>; |  | ||||||
| }) { |  | ||||||
|   return ( |  | ||||||
|     <label className="block"> |  | ||||||
|       <div className="text-sm font-medium text-gray-600 mb-1">{label}</div> |  | ||||||
|       <input |  | ||||||
|         type={type} |  | ||||||
|         {...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" |  | ||||||
|       /> |  | ||||||
|     </label> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ | const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ | ||||||
|   register, |   register, | ||||||
| }) => ( | }) => ( | ||||||
|  | @ -156,123 +103,6 @@ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ | ||||||
|   </div> |   </div> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ |  | ||||||
|   register, |  | ||||||
| }) => ( |  | ||||||
|   <div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl"> |  | ||||||
|     <HeadedSection title="General Display Settings"> |  | ||||||
|       <div className="space-y-4"> |  | ||||||
|         <Checkbox |  | ||||||
|           name="showEditor" |  | ||||||
|           register={register} |  | ||||||
|           label="Show code editor on left" |  | ||||||
|         /> |  | ||||||
|         <InputItem |  | ||||||
|           name="chartHeight" |  | ||||||
|           type="number" |  | ||||||
|           register={register} |  | ||||||
|           label="Chart Height (in pixels)" |  | ||||||
|         /> |  | ||||||
|       </div> |  | ||||||
|     </HeadedSection> |  | ||||||
| 
 |  | ||||||
|     <div className="pt-8"> |  | ||||||
|       <HeadedSection title="Distribution Display Settings"> |  | ||||||
|         <div className="space-y-2"> |  | ||||||
|           <Checkbox |  | ||||||
|             register={register} |  | ||||||
|             name="logX" |  | ||||||
|             label="Show x scale logarithmically" |  | ||||||
|           /> |  | ||||||
|           <Checkbox |  | ||||||
|             register={register} |  | ||||||
|             name="expY" |  | ||||||
|             label="Show y scale exponentially" |  | ||||||
|           /> |  | ||||||
|           <Checkbox |  | ||||||
|             register={register} |  | ||||||
|             name="distributionChartActions" |  | ||||||
|             label="Show vega chart controls" |  | ||||||
|           /> |  | ||||||
|           <Checkbox |  | ||||||
|             register={register} |  | ||||||
|             name="showControls" |  | ||||||
|             label="Show toggles to adjust scale of x and y axes" |  | ||||||
|           /> |  | ||||||
|           <Checkbox |  | ||||||
|             register={register} |  | ||||||
|             name="showSummary" |  | ||||||
|             label="Show summary statistics" |  | ||||||
|           /> |  | ||||||
|           <InputItem |  | ||||||
|             name="minX" |  | ||||||
|             type="number" |  | ||||||
|             register={register} |  | ||||||
|             label="Min X Value" |  | ||||||
|           /> |  | ||||||
|           <InputItem |  | ||||||
|             name="maxX" |  | ||||||
|             type="number" |  | ||||||
|             register={register} |  | ||||||
|             label="Max X Value" |  | ||||||
|           /> |  | ||||||
|           <InputItem |  | ||||||
|             name="title" |  | ||||||
|             type="text" |  | ||||||
|             register={register} |  | ||||||
|             label="Title" |  | ||||||
|           /> |  | ||||||
|           <InputItem |  | ||||||
|             name="tickFormat" |  | ||||||
|             type="text" |  | ||||||
|             register={register} |  | ||||||
|             label="Tick Format" |  | ||||||
|           /> |  | ||||||
|           <InputItem |  | ||||||
|             name="color" |  | ||||||
|             type="color" |  | ||||||
|             register={register} |  | ||||||
|             label="Color" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </HeadedSection> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <div className="pt-8"> |  | ||||||
|       <HeadedSection title="Function Display Settings"> |  | ||||||
|         <div className="space-y-6"> |  | ||||||
|           <Text> |  | ||||||
|             When displaying functions of single variables that return numbers or |  | ||||||
|             distributions, we need to use defaults for the x-axis. We need to |  | ||||||
|             select a minimum and maximum value of x to sample, and a number n of |  | ||||||
|             the number of points to sample. |  | ||||||
|           </Text> |  | ||||||
|           <div className="space-y-4"> |  | ||||||
|             <InputItem |  | ||||||
|               type="number" |  | ||||||
|               name="diagramStart" |  | ||||||
|               register={register} |  | ||||||
|               label="Min X Value" |  | ||||||
|             /> |  | ||||||
|             <InputItem |  | ||||||
|               type="number" |  | ||||||
|               name="diagramStop" |  | ||||||
|               register={register} |  | ||||||
|               label="Max X Value" |  | ||||||
|             /> |  | ||||||
|             <InputItem |  | ||||||
|               type="number" |  | ||||||
|               name="diagramCount" |  | ||||||
|               register={register} |  | ||||||
|               label="Points between X min and X max to sample" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </HeadedSection> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const InputVariablesSettings: React.FC<{ | const InputVariablesSettings: React.FC<{ | ||||||
|   initialImports: any; // TODO - any json type
 |   initialImports: any; // TODO - any json type
 | ||||||
|   setImports: (imports: any) => void; |   setImports: (imports: any) => void; | ||||||
|  | @ -402,15 +232,14 @@ const useRunnerState = (code: string) => { | ||||||
| export const SquigglePlayground: FC<PlaygroundProps> = ({ | export const SquigglePlayground: FC<PlaygroundProps> = ({ | ||||||
|   defaultCode = "", |   defaultCode = "", | ||||||
|   height = 500, |   height = 500, | ||||||
|   showControls = false, |  | ||||||
|   showSummary = false, |   showSummary = false, | ||||||
|   logX = false, |   logX = false, | ||||||
|   expY = false, |   expY = false, | ||||||
|   title, |   title, | ||||||
|   minX, |   minX, | ||||||
|   maxX, |   maxX, | ||||||
|   color = "#739ECC", |   color = defaultColor, | ||||||
|   tickFormat = ".9~s", |   tickFormat = defaultTickFormat, | ||||||
|   distributionChartActions, |   distributionChartActions, | ||||||
|   code: controlledCode, |   code: controlledCode, | ||||||
|   onCodeChange, |   onCodeChange, | ||||||
|  | @ -431,7 +260,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({ | ||||||
|       sampleCount: 1000, |       sampleCount: 1000, | ||||||
|       xyPointLength: 1000, |       xyPointLength: 1000, | ||||||
|       chartHeight: 150, |       chartHeight: 150, | ||||||
|       showControls, |  | ||||||
|       logX, |       logX, | ||||||
|       expY, |       expY, | ||||||
|       title, |       title, | ||||||
|  | @ -442,8 +270,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({ | ||||||
|       distributionChartActions, |       distributionChartActions, | ||||||
|       showSummary, |       showSummary, | ||||||
|       showEditor, |       showEditor, | ||||||
|       leftSizePercent: 50, |  | ||||||
|       showSettingsPage: false, |  | ||||||
|       diagramStart: 0, |       diagramStart: 0, | ||||||
|       diagramStop: 10, |       diagramStop: 10, | ||||||
|       diagramCount: 20, |       diagramCount: 20, | ||||||
|  | @ -500,7 +326,13 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({ | ||||||
|         <SamplingSettings register={register} /> |         <SamplingSettings register={register} /> | ||||||
|       </StyledTab.Panel> |       </StyledTab.Panel> | ||||||
|       <StyledTab.Panel> |       <StyledTab.Panel> | ||||||
|         <ViewSettings register={register} /> |         <ViewSettings | ||||||
|  |           register={ | ||||||
|  |             register as unknown as UseFormRegister< | ||||||
|  |               yup.InferType<typeof viewSettingsSchema> | ||||||
|  |             > | ||||||
|  |           } | ||||||
|  |         /> | ||||||
|       </StyledTab.Panel> |       </StyledTab.Panel> | ||||||
|       <StyledTab.Panel> |       <StyledTab.Panel> | ||||||
|         <InputVariablesSettings |         <InputVariablesSettings | ||||||
|  |  | ||||||
|  | @ -6,6 +6,8 @@ import { FunctionChart, FunctionChartSettings } from "../FunctionChart"; | ||||||
| import clsx from "clsx"; | import clsx from "clsx"; | ||||||
| import { VariableBox } from "./VariableBox"; | import { VariableBox } from "./VariableBox"; | ||||||
| import { ItemSettingsMenu } from "./ItemSettingsMenu"; | import { ItemSettingsMenu } from "./ItemSettingsMenu"; | ||||||
|  | import { hasMassBelowZero } from "../../lib/distributionUtils"; | ||||||
|  | import { MergedItemSettings } from "./utils"; | ||||||
| 
 | 
 | ||||||
| function getRange<a>(x: declaration<a>) { | function getRange<a>(x: declaration<a>) { | ||||||
|   const first = x.args[0]; |   const first = x.args[0]; | ||||||
|  | @ -33,12 +35,12 @@ function getChartSettings<a>(x: declaration<a>): FunctionChartSettings { | ||||||
| const VariableList: React.FC<{ | const VariableList: React.FC<{ | ||||||
|   path: string[]; |   path: string[]; | ||||||
|   heading: string; |   heading: string; | ||||||
|   children: React.ReactNode; |   children: (settings: MergedItemSettings) => React.ReactNode; | ||||||
| }> = ({ path, heading, children }) => ( | }> = ({ path, heading, children }) => ( | ||||||
|   <VariableBox path={path} heading={heading}> |   <VariableBox path={path} heading={heading}> | ||||||
|     {() => ( |     {(settings) => ( | ||||||
|       <div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}> |       <div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}> | ||||||
|         {children} |         {children(settings)} | ||||||
|       </div> |       </div> | ||||||
|     )} |     )} | ||||||
|   </VariableBox> |   </VariableBox> | ||||||
|  | @ -50,14 +52,12 @@ export interface Props { | ||||||
|   /** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */ |   /** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */ | ||||||
|   path: string[]; |   path: string[]; | ||||||
|   width?: number; |   width?: number; | ||||||
|   height: number; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ExpressionViewer: React.FC<Props> = ({ | export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|   path, |   path, | ||||||
|   expression, |   expression, | ||||||
|   width, |   width, | ||||||
|   height, |  | ||||||
| }) => { | }) => { | ||||||
|   switch (expression.tag) { |   switch (expression.tag) { | ||||||
|     case "number": |     case "number": | ||||||
|  | @ -78,9 +78,17 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|           heading={`Distribution (${distType})\n${ |           heading={`Distribution (${distType})\n${ | ||||||
|             distType === "Symbolic" ? expression.value.toString() : "" |             distType === "Symbolic" ? expression.value.toString() : "" | ||||||
|           }`}
 |           }`}
 | ||||||
|           dropdownMenu={({ settings, setSettings }) => { |           renderSettingsMenu={({ onChange }) => { | ||||||
|  |             const shape = expression.value.pointSet(); | ||||||
|             return ( |             return ( | ||||||
|               <ItemSettingsMenu settings={settings} setSettings={setSettings} /> |               <ItemSettingsMenu | ||||||
|  |                 path={path} | ||||||
|  |                 onChange={onChange} | ||||||
|  |                 disableLogX={ | ||||||
|  |                   shape.tag === "Ok" && hasMassBelowZero(shape.value) | ||||||
|  |                 } | ||||||
|  |                 withFunctionSettings={false} | ||||||
|  |               /> | ||||||
|             ); |             ); | ||||||
|           }} |           }} | ||||||
|         > |         > | ||||||
|  | @ -89,7 +97,7 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|               <DistributionChart |               <DistributionChart | ||||||
|                 distribution={expression.value} |                 distribution={expression.value} | ||||||
|                 {...settings.distributionPlotSettings} |                 {...settings.distributionPlotSettings} | ||||||
|                 height={height} |                 height={settings.height} | ||||||
|                 width={width} |                 width={width} | ||||||
|               /> |               /> | ||||||
|             ); |             ); | ||||||
|  | @ -158,9 +166,13 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|         <VariableBox |         <VariableBox | ||||||
|           path={path} |           path={path} | ||||||
|           heading="Function" |           heading="Function" | ||||||
|           dropdownMenu={({ settings, setSettings }) => { |           renderSettingsMenu={({ onChange }) => { | ||||||
|             return ( |             return ( | ||||||
|               <ItemSettingsMenu settings={settings} setSettings={setSettings} /> |               <ItemSettingsMenu | ||||||
|  |                 path={path} | ||||||
|  |                 onChange={onChange} | ||||||
|  |                 withFunctionSettings={true} | ||||||
|  |               /> | ||||||
|             ); |             ); | ||||||
|           }} |           }} | ||||||
|         > |         > | ||||||
|  | @ -173,7 +185,7 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|                 fn={expression.value} |                 fn={expression.value} | ||||||
|                 chartSettings={settings.chartSettings} |                 chartSettings={settings.chartSettings} | ||||||
|                 distributionPlotSettings={settings.distributionPlotSettings} |                 distributionPlotSettings={settings.distributionPlotSettings} | ||||||
|                 height={height} |                 height={settings.height} | ||||||
|                 environment={{ |                 environment={{ | ||||||
|                   sampleCount: settings.environment.sampleCount / 10, |                   sampleCount: settings.environment.sampleCount / 10, | ||||||
|                   xyPointLength: settings.environment.xyPointLength / 10, |                   xyPointLength: settings.environment.xyPointLength / 10, | ||||||
|  | @ -188,9 +200,13 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|         <VariableBox |         <VariableBox | ||||||
|           path={path} |           path={path} | ||||||
|           heading="Function Declaration" |           heading="Function Declaration" | ||||||
|           dropdownMenu={({ settings, setSettings }) => { |           renderSettingsMenu={({ onChange }) => { | ||||||
|             return ( |             return ( | ||||||
|               <ItemSettingsMenu settings={settings} setSettings={setSettings} /> |               <ItemSettingsMenu | ||||||
|  |                 onChange={onChange} | ||||||
|  |                 path={path} | ||||||
|  |                 withFunctionSettings={true} | ||||||
|  |               /> | ||||||
|             ); |             ); | ||||||
|           }} |           }} | ||||||
|         > |         > | ||||||
|  | @ -199,7 +215,7 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|               fn={expression.value.fn} |               fn={expression.value.fn} | ||||||
|               chartSettings={getChartSettings(expression.value)} |               chartSettings={getChartSettings(expression.value)} | ||||||
|               distributionPlotSettings={settings.distributionPlotSettings} |               distributionPlotSettings={settings.distributionPlotSettings} | ||||||
|               height={height} |               height={settings.height} | ||||||
|               environment={{ |               environment={{ | ||||||
|                 sampleCount: settings.environment.sampleCount / 10, |                 sampleCount: settings.environment.sampleCount / 10, | ||||||
|                 xyPointLength: settings.environment.xyPointLength / 10, |                 xyPointLength: settings.environment.xyPointLength / 10, | ||||||
|  | @ -212,7 +228,8 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|     case "module": { |     case "module": { | ||||||
|       return ( |       return ( | ||||||
|         <VariableList path={path} heading="Module"> |         <VariableList path={path} heading="Module"> | ||||||
|           {Object.entries(expression.value) |           {(settings) => | ||||||
|  |             Object.entries(expression.value) | ||||||
|               .filter(([key, r]) => key !== "Math") |               .filter(([key, r]) => key !== "Math") | ||||||
|               .map(([key, r]) => ( |               .map(([key, r]) => ( | ||||||
|                 <ExpressionViewer |                 <ExpressionViewer | ||||||
|  | @ -220,38 +237,40 @@ export const ExpressionViewer: React.FC<Props> = ({ | ||||||
|                   path={[...path, key]} |                   path={[...path, key]} | ||||||
|                   expression={r} |                   expression={r} | ||||||
|                   width={width !== undefined ? width - 20 : width} |                   width={width !== undefined ? width - 20 : width} | ||||||
|                 height={height / 3} |  | ||||||
|                 /> |                 /> | ||||||
|             ))} |               )) | ||||||
|  |           } | ||||||
|         </VariableList> |         </VariableList> | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     case "record": |     case "record": | ||||||
|       return ( |       return ( | ||||||
|         <VariableList path={path} heading="Record"> |         <VariableList path={path} heading="Record"> | ||||||
|           {Object.entries(expression.value).map(([key, r]) => ( |           {(settings) => | ||||||
|  |             Object.entries(expression.value).map(([key, r]) => ( | ||||||
|               <ExpressionViewer |               <ExpressionViewer | ||||||
|                 key={key} |                 key={key} | ||||||
|                 path={[...path, key]} |                 path={[...path, key]} | ||||||
|                 expression={r} |                 expression={r} | ||||||
|                 width={width !== undefined ? width - 20 : width} |                 width={width !== undefined ? width - 20 : width} | ||||||
|               height={height / 3} |  | ||||||
|               /> |               /> | ||||||
|           ))} |             )) | ||||||
|  |           } | ||||||
|         </VariableList> |         </VariableList> | ||||||
|       ); |       ); | ||||||
|     case "array": |     case "array": | ||||||
|       return ( |       return ( | ||||||
|         <VariableList path={path} heading="Array"> |         <VariableList path={path} heading="Array"> | ||||||
|           {expression.value.map((r, i) => ( |           {(settings) => | ||||||
|  |             expression.value.map((r, i) => ( | ||||||
|               <ExpressionViewer |               <ExpressionViewer | ||||||
|                 key={i} |                 key={i} | ||||||
|                 path={[...path, String(i)]} |                 path={[...path, String(i)]} | ||||||
|                 expression={r} |                 expression={r} | ||||||
|                 width={width !== undefined ? width - 20 : width} |                 width={width !== undefined ? width - 20 : width} | ||||||
|               height={50} |  | ||||||
|               /> |               /> | ||||||
|           ))} |             )) | ||||||
|  |           } | ||||||
|         </VariableList> |         </VariableList> | ||||||
|       ); |       ); | ||||||
|     default: { |     default: { | ||||||
|  |  | ||||||
|  | @ -1,73 +1,127 @@ | ||||||
| import React from "react"; | import { CogIcon } from "@heroicons/react/solid"; | ||||||
| import { DropdownMenu } from "../ui/DropdownMenu"; | import React, { useContext, useState } from "react"; | ||||||
| import { LocalItemSettings } from "./utils"; | import { useForm } from "react-hook-form"; | ||||||
|  | import { yupResolver } from "@hookform/resolvers/yup"; | ||||||
|  | import { Modal } from "../ui/Modal"; | ||||||
|  | import { ViewSettings, viewSettingsSchema } from "../ViewSettings"; | ||||||
|  | import { Path, pathAsString } from "./utils"; | ||||||
|  | import { ViewerContext } from "./ViewerContext"; | ||||||
|  | import { | ||||||
|  |   defaultColor, | ||||||
|  |   defaultTickFormat, | ||||||
|  | } from "../../lib/distributionSpecBuilder"; | ||||||
| 
 | 
 | ||||||
| type Props = { | type Props = { | ||||||
|   settings: LocalItemSettings; |   path: Path; | ||||||
|   setSettings: (value: LocalItemSettings) => void; |   onChange: () => void; | ||||||
|  |   disableLogX?: boolean; | ||||||
|  |   withFunctionSettings: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const ItemSettingsMenu: React.FC<Props> = ({ | const ItemSettingsModal: React.FC<Props & { close: () => void }> = ({ | ||||||
|   settings, |   path, | ||||||
|   setSettings, |   onChange, | ||||||
|  |   disableLogX, | ||||||
|  |   withFunctionSettings, | ||||||
|  |   close, | ||||||
| }) => { | }) => { | ||||||
|  |   const { setSettings, getSettings, getMergedSettings } = | ||||||
|  |     useContext(ViewerContext); | ||||||
|  | 
 | ||||||
|  |   const mergedSettings = getMergedSettings(path); | ||||||
|  | 
 | ||||||
|  |   const { register, watch } = useForm({ | ||||||
|  |     resolver: yupResolver(viewSettingsSchema), | ||||||
|  |     defaultValues: { | ||||||
|  |       showEditor: true, // doesn't matter
 | ||||||
|  |       chartHeight: mergedSettings.height, | ||||||
|  |       showSummary: mergedSettings.distributionPlotSettings.showSummary, | ||||||
|  |       logX: mergedSettings.distributionPlotSettings.logX, | ||||||
|  |       expY: mergedSettings.distributionPlotSettings.expY, | ||||||
|  |       tickFormat: | ||||||
|  |         mergedSettings.distributionPlotSettings.format || defaultTickFormat, | ||||||
|  |       title: mergedSettings.distributionPlotSettings.title, | ||||||
|  |       color: mergedSettings.distributionPlotSettings.color || defaultColor, | ||||||
|  |       minX: mergedSettings.distributionPlotSettings.minX, | ||||||
|  |       maxX: mergedSettings.distributionPlotSettings.maxX, | ||||||
|  |       distributionChartActions: mergedSettings.distributionPlotSettings.actions, | ||||||
|  |       diagramStart: mergedSettings.chartSettings.start, | ||||||
|  |       diagramStop: mergedSettings.chartSettings.stop, | ||||||
|  |       diagramCount: mergedSettings.chartSettings.count, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     const subscription = watch((vars) => { | ||||||
|  |       const settings = getSettings(path); // get the latest version
 | ||||||
|  |       setSettings(path, { | ||||||
|  |         ...settings, | ||||||
|  |         distributionPlotSettings: { | ||||||
|  |           showSummary: vars.showSummary, | ||||||
|  |           logX: vars.logX, | ||||||
|  |           expY: vars.expY, | ||||||
|  |           format: vars.tickFormat, | ||||||
|  |           title: vars.title, | ||||||
|  |           color: vars.color, | ||||||
|  |           minX: vars.minX, | ||||||
|  |           maxX: vars.maxX, | ||||||
|  |           actions: vars.distributionChartActions, | ||||||
|  |         }, | ||||||
|  |         chartSettings: { | ||||||
|  |           start: vars.diagramStart, | ||||||
|  |           stop: vars.diagramStop, | ||||||
|  |           count: vars.diagramCount, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |       onChange(); | ||||||
|  |     }); | ||||||
|  |     return () => subscription.unsubscribe(); | ||||||
|  |   }, [getSettings, setSettings, onChange, path, watch]); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex gap-1 items-center"> |     <Modal> | ||||||
|       <DropdownMenu> |       <Modal.Header close={close}> | ||||||
|         <DropdownMenu.CheckboxItem |         Chart settings{path.length ? " for " + pathAsString(path) : ""} | ||||||
|           label="Log X scale" |       </Modal.Header> | ||||||
|           value={settings.distributionPlotSettings?.logX ?? false} |       <Modal.Body> | ||||||
|           toggle={() => |         <ViewSettings | ||||||
|             setSettings({ |           register={register} | ||||||
|               ...settings, |           withShowEditorSetting={false} | ||||||
|               distributionPlotSettings: { |           withFunctionSettings={withFunctionSettings} | ||||||
|                 ...settings.distributionPlotSettings, |  | ||||||
|                 logX: !settings.distributionPlotSettings?.logX, |  | ||||||
|               }, |  | ||||||
|             }) |  | ||||||
|           } |  | ||||||
|         /> |         /> | ||||||
|         <DropdownMenu.CheckboxItem |       </Modal.Body> | ||||||
|           label="Exp Y scale" |     </Modal> | ||||||
|           value={settings.distributionPlotSettings?.expY ?? false} |   ); | ||||||
|           toggle={() => | }; | ||||||
|             setSettings({ | 
 | ||||||
|               ...settings, | export const ItemSettingsMenu: React.FC<Props> = (props) => { | ||||||
|               distributionPlotSettings: { |   const [isOpen, setIsOpen] = useState(false); | ||||||
|                 ...settings.distributionPlotSettings, |   const { setSettings, getSettings } = useContext(ViewerContext); | ||||||
|                 expY: !settings.distributionPlotSettings?.expY, |   const settings = getSettings(props.path); | ||||||
|               }, | 
 | ||||||
|             }) |   return ( | ||||||
|           } |     <div className="flex gap-2"> | ||||||
|  |       <CogIcon | ||||||
|  |         className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500" | ||||||
|  |         onClick={() => setIsOpen(!isOpen)} | ||||||
|       /> |       /> | ||||||
|         <DropdownMenu.CheckboxItem |  | ||||||
|           label="Show summary" |  | ||||||
|           value={settings.distributionPlotSettings?.showSummary ?? false} |  | ||||||
|           toggle={() => |  | ||||||
|             setSettings({ |  | ||||||
|               ...settings, |  | ||||||
|               distributionPlotSettings: { |  | ||||||
|                 ...settings.distributionPlotSettings, |  | ||||||
|                 showSummary: !settings.distributionPlotSettings?.showSummary, |  | ||||||
|               }, |  | ||||||
|             }) |  | ||||||
|           } |  | ||||||
|         /> |  | ||||||
|       </DropdownMenu> |  | ||||||
|       {settings.distributionPlotSettings || settings.chartSettings ? ( |       {settings.distributionPlotSettings || settings.chartSettings ? ( | ||||||
|         <button |         <button | ||||||
|           onClick={() => |           onClick={() => { | ||||||
|             setSettings({ |             setSettings(props.path, { | ||||||
|               ...settings, |               ...settings, | ||||||
|               distributionPlotSettings: undefined, |               distributionPlotSettings: undefined, | ||||||
|               chartSettings: undefined, |               chartSettings: undefined, | ||||||
|             }) |             }); | ||||||
|           } |             props.onChange(); | ||||||
|  |           }} | ||||||
|           className="text-xs px-1 py-0.5 rounded bg-slate-300" |           className="text-xs px-1 py-0.5 rounded bg-slate-300" | ||||||
|         > |         > | ||||||
|           Reset settings |           Reset settings | ||||||
|         </button> |         </button> | ||||||
|       ) : null} |       ) : null} | ||||||
|  |       {isOpen ? ( | ||||||
|  |         <ItemSettingsModal {...props} close={() => setIsOpen(false)} /> | ||||||
|  |       ) : null} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -3,22 +3,21 @@ import { Tooltip } from "../ui/Tooltip"; | ||||||
| import { LocalItemSettings, MergedItemSettings } from "./utils"; | import { LocalItemSettings, MergedItemSettings } from "./utils"; | ||||||
| import { ViewerContext } from "./ViewerContext"; | import { ViewerContext } from "./ViewerContext"; | ||||||
| 
 | 
 | ||||||
| type DropdownMenuParams = { | type SettingsMenuParams = { | ||||||
|   settings: LocalItemSettings; |   onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself
 | ||||||
|   setSettings: (value: LocalItemSettings) => void; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type VariableBoxProps = { | type VariableBoxProps = { | ||||||
|   path: string[]; |   path: string[]; | ||||||
|   heading: string; |   heading: string; | ||||||
|   dropdownMenu?: (params: DropdownMenuParams) => React.ReactNode; |   renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode; | ||||||
|   children: (settings: MergedItemSettings) => React.ReactNode; |   children: (settings: MergedItemSettings) => React.ReactNode; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const VariableBox: React.FC<VariableBoxProps> = ({ | export const VariableBox: React.FC<VariableBoxProps> = ({ | ||||||
|   path, |   path, | ||||||
|   heading = "Error", |   heading = "Error", | ||||||
|   dropdownMenu, |   renderSettingsMenu, | ||||||
|   children, |   children, | ||||||
| }) => { | }) => { | ||||||
|   const { setSettings, getSettings, getMergedSettings } = |   const { setSettings, getSettings, getMergedSettings } = | ||||||
|  | @ -57,8 +56,8 @@ export const VariableBox: React.FC<VariableBoxProps> = ({ | ||||||
|           > |           > | ||||||
|             ... |             ... | ||||||
|           </span> |           </span> | ||||||
|         ) : dropdownMenu ? ( |         ) : renderSettingsMenu ? ( | ||||||
|           dropdownMenu({ settings, setSettings: setSettingsAndUpdate }) |           renderSettingsMenu({ onChange: forceUpdate }) | ||||||
|         ) : null} |         ) : null} | ||||||
|       </header> |       </header> | ||||||
|       {settings.collapsed ? null : ( |       {settings.collapsed ? null : ( | ||||||
|  |  | ||||||
|  | @ -23,11 +23,11 @@ export const ViewerContext = React.createContext<ViewerContextShape>({ | ||||||
|     }, |     }, | ||||||
|     distributionPlotSettings: { |     distributionPlotSettings: { | ||||||
|       showSummary: false, |       showSummary: false, | ||||||
|       showControls: false, |  | ||||||
|       logX: false, |       logX: false, | ||||||
|       expY: false, |       expY: false, | ||||||
|     }, |     }, | ||||||
|     environment: defaultEnvironment, |     environment: defaultEnvironment, | ||||||
|  |     height: 150, | ||||||
|   }), |   }), | ||||||
|   setSettings() {}, |   setSettings() {}, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -72,10 +72,11 @@ export const SquiggleViewer: React.FC<Props> = ({ | ||||||
|           ...environment, |           ...environment, | ||||||
|           ...(localSettings.environment || {}), |           ...(localSettings.environment || {}), | ||||||
|         }, |         }, | ||||||
|  |         height: localSettings.height || height, | ||||||
|       }; |       }; | ||||||
|       return result; |       return result; | ||||||
|     }, |     }, | ||||||
|     [distributionPlotSettings, chartSettings, environment, getSettings] |     [distributionPlotSettings, chartSettings, environment, height, getSettings] | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | @ -87,12 +88,7 @@ export const SquiggleViewer: React.FC<Props> = ({ | ||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|       {result.tag === "Ok" ? ( |       {result.tag === "Ok" ? ( | ||||||
|         <ExpressionViewer |         <ExpressionViewer path={[]} expression={result.value} width={width} /> | ||||||
|           path={[]} |  | ||||||
|           expression={result.value} |  | ||||||
|           width={width} |  | ||||||
|           height={height} |  | ||||||
|         /> |  | ||||||
|       ) : ( |       ) : ( | ||||||
|         <SquiggleErrorAlert error={result.value} /> |         <SquiggleErrorAlert error={result.value} /> | ||||||
|       )} |       )} | ||||||
|  |  | ||||||
|  | @ -6,12 +6,14 @@ export type LocalItemSettings = { | ||||||
|   collapsed: boolean; |   collapsed: boolean; | ||||||
|   distributionPlotSettings?: Partial<DistributionPlottingSettings>; |   distributionPlotSettings?: Partial<DistributionPlottingSettings>; | ||||||
|   chartSettings?: Partial<FunctionChartSettings>; |   chartSettings?: Partial<FunctionChartSettings>; | ||||||
|  |   height?: number; | ||||||
|   environment?: Partial<environment>; |   environment?: Partial<environment>; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type MergedItemSettings = { | export type MergedItemSettings = { | ||||||
|   distributionPlotSettings: DistributionPlottingSettings; |   distributionPlotSettings: DistributionPlottingSettings; | ||||||
|   chartSettings: FunctionChartSettings; |   chartSettings: FunctionChartSettings; | ||||||
|  |   height: number; | ||||||
|   environment: environment; |   environment: environment; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										155
									
								
								packages/components/src/components/ViewSettings.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								packages/components/src/components/ViewSettings.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,155 @@ | ||||||
|  | import React from "react"; | ||||||
|  | import * as yup from "yup"; | ||||||
|  | import { UseFormRegister } from "react-hook-form"; | ||||||
|  | import { InputItem } from "./ui/InputItem"; | ||||||
|  | import { Checkbox } from "./ui/Checkbox"; | ||||||
|  | import { HeadedSection } from "./ui/HeadedSection"; | ||||||
|  | import { Text } from "./ui/Text"; | ||||||
|  | import { | ||||||
|  |   defaultColor, | ||||||
|  |   defaultTickFormat, | ||||||
|  | } from "../lib/distributionSpecBuilder"; | ||||||
|  | 
 | ||||||
|  | export const viewSettingsSchema = yup.object({}).shape({ | ||||||
|  |   chartHeight: yup.number().required().positive().integer().default(350), | ||||||
|  |   showSummary: yup.boolean().required(), | ||||||
|  |   showEditor: yup.boolean().required(), | ||||||
|  |   logX: yup.boolean().required(), | ||||||
|  |   expY: yup.boolean().required(), | ||||||
|  |   tickFormat: yup.string().default(defaultTickFormat), | ||||||
|  |   title: yup.string(), | ||||||
|  |   color: yup.string().default(defaultColor).required(), | ||||||
|  |   minX: yup.number(), | ||||||
|  |   maxX: yup.number(), | ||||||
|  |   distributionChartActions: yup.boolean(), | ||||||
|  |   diagramStart: yup.number().required().positive().integer().default(0).min(0), | ||||||
|  |   diagramStop: yup.number().required().positive().integer().default(10).min(0), | ||||||
|  |   diagramCount: yup.number().required().positive().integer().default(20).min(2), | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | type FormFields = yup.InferType<typeof viewSettingsSchema>; | ||||||
|  | 
 | ||||||
|  | // This component is used in two places: for global settings in SquigglePlayground, and for item-specific settings in modal dialogs.
 | ||||||
|  | export const ViewSettings: React.FC<{ | ||||||
|  |   withShowEditorSetting?: boolean; | ||||||
|  |   withFunctionSettings?: boolean; | ||||||
|  |   register: UseFormRegister<FormFields>; | ||||||
|  | }> = ({ | ||||||
|  |   withShowEditorSetting = true, | ||||||
|  |   withFunctionSettings = true, | ||||||
|  |   register, | ||||||
|  | }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl"> | ||||||
|  |       <HeadedSection title="General Display Settings"> | ||||||
|  |         <div className="space-y-4"> | ||||||
|  |           {withShowEditorSetting ? ( | ||||||
|  |             <Checkbox | ||||||
|  |               name="showEditor" | ||||||
|  |               register={register} | ||||||
|  |               label="Show code editor on left" | ||||||
|  |             /> | ||||||
|  |           ) : null} | ||||||
|  |           <InputItem | ||||||
|  |             name="chartHeight" | ||||||
|  |             type="number" | ||||||
|  |             register={register} | ||||||
|  |             label="Chart Height (in pixels)" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </HeadedSection> | ||||||
|  | 
 | ||||||
|  |       <div className="pt-8"> | ||||||
|  |         <HeadedSection title="Distribution Display Settings"> | ||||||
|  |           <div className="space-y-2"> | ||||||
|  |             <Checkbox | ||||||
|  |               register={register} | ||||||
|  |               name="logX" | ||||||
|  |               label="Show x scale logarithmically" | ||||||
|  |             /> | ||||||
|  |             <Checkbox | ||||||
|  |               register={register} | ||||||
|  |               name="expY" | ||||||
|  |               label="Show y scale exponentially" | ||||||
|  |             /> | ||||||
|  |             <Checkbox | ||||||
|  |               register={register} | ||||||
|  |               name="distributionChartActions" | ||||||
|  |               label="Show vega chart controls" | ||||||
|  |             /> | ||||||
|  |             <Checkbox | ||||||
|  |               register={register} | ||||||
|  |               name="showSummary" | ||||||
|  |               label="Show summary statistics" | ||||||
|  |             /> | ||||||
|  |             <InputItem | ||||||
|  |               name="minX" | ||||||
|  |               type="number" | ||||||
|  |               register={register} | ||||||
|  |               label="Min X Value" | ||||||
|  |             /> | ||||||
|  |             <InputItem | ||||||
|  |               name="maxX" | ||||||
|  |               type="number" | ||||||
|  |               register={register} | ||||||
|  |               label="Max X Value" | ||||||
|  |             /> | ||||||
|  |             <InputItem | ||||||
|  |               name="title" | ||||||
|  |               type="text" | ||||||
|  |               register={register} | ||||||
|  |               label="Title" | ||||||
|  |             /> | ||||||
|  |             <InputItem | ||||||
|  |               name="tickFormat" | ||||||
|  |               type="text" | ||||||
|  |               register={register} | ||||||
|  |               label="Tick Format" | ||||||
|  |             /> | ||||||
|  |             <InputItem | ||||||
|  |               name="color" | ||||||
|  |               type="color" | ||||||
|  |               register={register} | ||||||
|  |               label="Color" | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </HeadedSection> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       {withFunctionSettings ? ( | ||||||
|  |         <div className="pt-8"> | ||||||
|  |           <HeadedSection title="Function Display Settings"> | ||||||
|  |             <div className="space-y-6"> | ||||||
|  |               <Text> | ||||||
|  |                 When displaying functions of single variables that return | ||||||
|  |                 numbers or distributions, we need to use defaults for the | ||||||
|  |                 x-axis. We need to select a minimum and maximum value of x to | ||||||
|  |                 sample, and a number n of the number of points to sample. | ||||||
|  |               </Text> | ||||||
|  |               <div className="space-y-4"> | ||||||
|  |                 <InputItem | ||||||
|  |                   type="number" | ||||||
|  |                   name="diagramStart" | ||||||
|  |                   register={register} | ||||||
|  |                   label="Min X Value" | ||||||
|  |                 /> | ||||||
|  |                 <InputItem | ||||||
|  |                   type="number" | ||||||
|  |                   name="diagramStop" | ||||||
|  |                   register={register} | ||||||
|  |                   label="Max X Value" | ||||||
|  |                 /> | ||||||
|  |                 <InputItem | ||||||
|  |                   type="number" | ||||||
|  |                   name="diagramCount" | ||||||
|  |                   register={register} | ||||||
|  |                   label="Points between X min and X max to sample" | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </HeadedSection> | ||||||
|  |         </div> | ||||||
|  |       ) : null} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										13
									
								
								packages/components/src/components/ui/HeadedSection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/components/src/components/ui/HeadedSection.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import React from "react"; | ||||||
|  | 
 | ||||||
|  | export const HeadedSection: React.FC<{ | ||||||
|  |   title: string; | ||||||
|  |   children: React.ReactNode; | ||||||
|  | }> = ({ title, children }) => ( | ||||||
|  |   <div> | ||||||
|  |     <header className="text-lg leading-6 font-medium text-gray-900"> | ||||||
|  |       {title} | ||||||
|  |     </header> | ||||||
|  |     <div className="mt-4">{children}</div> | ||||||
|  |   </div> | ||||||
|  | ); | ||||||
							
								
								
									
										25
									
								
								packages/components/src/components/ui/InputItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								packages/components/src/components/ui/InputItem.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | ||||||
|  | import React from "react"; | ||||||
|  | import { Path, UseFormRegister } from "react-hook-form"; | ||||||
|  | 
 | ||||||
|  | export function InputItem<T>({ | ||||||
|  |   name, | ||||||
|  |   label, | ||||||
|  |   type, | ||||||
|  |   register, | ||||||
|  | }: { | ||||||
|  |   name: Path<T>; | ||||||
|  |   label: string; | ||||||
|  |   type: "number" | "text" | "color"; | ||||||
|  |   register: UseFormRegister<T>; | ||||||
|  | }) { | ||||||
|  |   return ( | ||||||
|  |     <label className="block"> | ||||||
|  |       <div className="text-sm font-medium text-gray-600 mb-1">{label}</div> | ||||||
|  |       <input | ||||||
|  |         type={type} | ||||||
|  |         {...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" | ||||||
|  |       /> | ||||||
|  |     </label> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								packages/components/src/components/ui/Modal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								packages/components/src/components/ui/Modal.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | ||||||
|  | import { motion } from "framer-motion"; | ||||||
|  | import * as React from "react"; | ||||||
|  | import * as ReactDOM from "react-dom"; | ||||||
|  | import { XIcon } from "@heroicons/react/solid"; | ||||||
|  | 
 | ||||||
|  | const Overlay: React.FC = () => ( | ||||||
|  |   <motion.div | ||||||
|  |     className="absolute inset-0 -z-10 bg-black" | ||||||
|  |     initial={{ opacity: 0 }} | ||||||
|  |     animate={{ opacity: 0.3 }} | ||||||
|  |   /> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const ModalHeader: React.FC<{ | ||||||
|  |   close: () => void; | ||||||
|  |   children: React.ReactNode; | ||||||
|  | }> = ({ children, close }) => { | ||||||
|  |   return ( | ||||||
|  |     <header className="px-5 py-3 border-b border-gray-200 font-bold flex items-center justify-between"> | ||||||
|  |       <div>{children}</div> | ||||||
|  |       <button | ||||||
|  |         className="px-1 bg-transparent cursor-pointer text-gray-700 hover:text-accent-500" | ||||||
|  |         type="button" | ||||||
|  |         onClick={close} | ||||||
|  |       > | ||||||
|  |         <XIcon className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500" /> | ||||||
|  |       </button> | ||||||
|  |     </header> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // TODO - get rid of forwardRef, support `focus` and `{...hotkeys}` via smart props
 | ||||||
|  | const ModalBody = React.forwardRef< | ||||||
|  |   HTMLDivElement, | ||||||
|  |   JSX.IntrinsicElements["div"] | ||||||
|  | >(function ModalBody(props, ref) { | ||||||
|  |   return <div ref={ref} className="px-5 py-3 overflow-auto" {...props} />; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const ModalFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => ( | ||||||
|  |   <div className="px-5 py-3 border-t border-gray-200">{children}</div> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const ModalWindow: React.FC<{ children: React.ReactNode }> = ({ children }) => ( | ||||||
|  |   <div | ||||||
|  |     className="bg-white rounded shadow-toast overflow-auto flex flex-col mx-2 w-96" | ||||||
|  |     style={{ maxHeight: "calc(100% - 20px)", maxWidth: "calc(100% - 20px)" }} | ||||||
|  |   > | ||||||
|  |     {children} | ||||||
|  |   </div> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | type ModalType = React.FC<{ children: React.ReactNode }> & { | ||||||
|  |   Body: typeof ModalBody; | ||||||
|  |   Footer: typeof ModalFooter; | ||||||
|  |   Header: typeof ModalHeader; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const Modal: ModalType = ({ children }) => { | ||||||
|  |   const [el] = React.useState(() => document.createElement("div")); | ||||||
|  | 
 | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     document.body.appendChild(el); | ||||||
|  | 
 | ||||||
|  |     return () => { | ||||||
|  |       document.body.removeChild(el); | ||||||
|  |     }; | ||||||
|  |   }, [el]); | ||||||
|  | 
 | ||||||
|  |   const modal = ( | ||||||
|  |     <div className="squiggle"> | ||||||
|  |       <div className="fixed inset-0 z-40 flex justify-center items-center"> | ||||||
|  |         <Overlay /> | ||||||
|  |         <ModalWindow>{children}</ModalWindow> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return ReactDOM.createPortal(modal, el); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Modal.Body = ModalBody; | ||||||
|  | Modal.Footer = ModalFooter; | ||||||
|  | Modal.Header = ModalHeader; | ||||||
							
								
								
									
										5
									
								
								packages/components/src/components/ui/Text.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packages/components/src/components/ui/Text.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import React from "react"; | ||||||
|  | 
 | ||||||
|  | export const Text: React.FC<{ children: React.ReactNode }> = ({ children }) => ( | ||||||
|  |   <p className="text-sm text-gray-500">{children}</p> | ||||||
|  | ); | ||||||
|  | @ -100,12 +100,15 @@ export let expYScale: PowScale = { | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const defaultTickFormat = ".9~s"; | ||||||
|  | export const defaultColor = "#739ECC"; | ||||||
|  | 
 | ||||||
| export function buildVegaSpec( | export function buildVegaSpec( | ||||||
|   specOptions: DistributionChartSpecOptions |   specOptions: DistributionChartSpecOptions | ||||||
| ): VisualizationSpec { | ): VisualizationSpec { | ||||||
|   let { |   let { | ||||||
|     format = ".9~s", |     format = defaultTickFormat, | ||||||
|     color = "#739ECC", |     color = defaultColor, | ||||||
|     title, |     title, | ||||||
|     minX, |     minX, | ||||||
|     maxX, |     maxX, | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								packages/components/src/lib/distributionUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packages/components/src/lib/distributionUtils.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | import { shape } from "@quri/squiggle-lang"; | ||||||
|  | 
 | ||||||
|  | export const hasMassBelowZero = (shape: shape) => | ||||||
|  |   shape.continuous.some((x) => x.x <= 0) || | ||||||
|  |   shape.discrete.some((x) => x.x <= 0); | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user