Merge branch 'develop' into reducer-project
This commit is contained in:
		
						commit
						bc86988453
					
				|  | @ -4,6 +4,8 @@ import { | |||
|   result, | ||||
|   distributionError, | ||||
|   distributionErrorToString, | ||||
|   squiggleExpression, | ||||
|   resultMap, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { Vega } from "react-vega"; | ||||
| import { ErrorAlert } from "./Alert"; | ||||
|  | @ -14,6 +16,8 @@ import { | |||
|   DistributionChartSpecOptions, | ||||
| } from "../lib/distributionSpecBuilder"; | ||||
| import { NumberShower } from "./NumberShower"; | ||||
| import { Plot, parsePlot } from "../lib/plotParser"; | ||||
| import { flattenResult } from "../lib/utility"; | ||||
| import { hasMassBelowZero } from "../lib/distributionUtils"; | ||||
| 
 | ||||
| export type DistributionPlottingSettings = { | ||||
|  | @ -23,26 +27,41 @@ export type DistributionPlottingSettings = { | |||
| } & DistributionChartSpecOptions; | ||||
| 
 | ||||
| export type DistributionChartProps = { | ||||
|   distribution: Distribution; | ||||
|   plot: Plot; | ||||
|   width?: number; | ||||
|   height: number; | ||||
| } & DistributionPlottingSettings; | ||||
| 
 | ||||
| export function defaultPlot(distribution: Distribution): Plot { | ||||
|   return { distributions: [{ name: "default", distribution }] }; | ||||
| } | ||||
| 
 | ||||
| export function makePlot(record: { | ||||
|   [key: string]: squiggleExpression; | ||||
| }): Plot | void { | ||||
|   const plotResult = parsePlot(record); | ||||
|   if (plotResult.tag === "Ok") { | ||||
|     return plotResult.value; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | ||||
|   const { | ||||
|     distribution, | ||||
|     height, | ||||
|     showSummary, | ||||
|     width, | ||||
|     logX, | ||||
|     actions = false, | ||||
|   } = props; | ||||
|   const shape = distribution.pointSet(); | ||||
|   const { plot, height, showSummary, width, logX, actions = false } = props; | ||||
|   const [sized] = useSize((size) => { | ||||
|     if (shape.tag === "Error") { | ||||
|     let shapes = flattenResult( | ||||
|       plot.distributions.map((x) => | ||||
|         resultMap(x.distribution.pointSet(), (shape) => ({ | ||||
|           name: x.name, | ||||
|           // color: x.color, // not supported yet
 | ||||
|           continuous: shape.continuous, | ||||
|           discrete: shape.discrete, | ||||
|         })) | ||||
|       ) | ||||
|     ); | ||||
|     if (shapes.tag === "Error") { | ||||
|       return ( | ||||
|         <ErrorAlert heading="Distribution Error"> | ||||
|           {distributionErrorToString(shape.value)} | ||||
|           {distributionErrorToString(shapes.value)} | ||||
|         </ErrorAlert> | ||||
|       ); | ||||
|     } | ||||
|  | @ -56,24 +75,29 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | |||
|       ); | ||||
|       widthProp = 20; | ||||
|     } | ||||
|     const domain = shapes.value.flatMap((shape) => | ||||
|       shape.discrete.concat(shape.continuous) | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <div style={{ width: widthProp }}> | ||||
|         {logX && hasMassBelowZero(shape.value) ? ( | ||||
|         {logX && shapes.value.some(hasMassBelowZero) ? ( | ||||
|           <ErrorAlert heading="Log Domain Error"> | ||||
|             Cannot graph distribution with negative values on logarithmic scale. | ||||
|           </ErrorAlert> | ||||
|         ) : ( | ||||
|           <Vega | ||||
|             spec={spec} | ||||
|             data={{ con: shape.value.continuous, dis: shape.value.discrete }} | ||||
|             data={{ data: shapes.value, domain }} | ||||
|             width={widthProp - 10} | ||||
|             height={height} | ||||
|             actions={actions} | ||||
|           /> | ||||
|         )} | ||||
|         <div className="flex justify-center"> | ||||
|           {showSummary && <SummaryTable distribution={distribution} />} | ||||
|           {showSummary && plot.distributions.length === 1 && ( | ||||
|             <SummaryTable distribution={plot.distributions[0].distribution} /> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; | |||
| import { | ||||
|   DistributionChart, | ||||
|   DistributionPlottingSettings, | ||||
|   defaultPlot, | ||||
| } from "./DistributionChart"; | ||||
| import { NumberShower } from "./NumberShower"; | ||||
| import { ErrorAlert } from "./Alert"; | ||||
|  | @ -179,7 +180,7 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({ | |||
|   let showChart = | ||||
|     mouseItem.tag === "Ok" && mouseItem.value.tag === "distribution" ? ( | ||||
|       <DistributionChart | ||||
|         distribution={mouseItem.value.value} | ||||
|         plot={defaultPlot(mouseItem.value.value)} | ||||
|         width={400} | ||||
|         height={50} | ||||
|         {...distributionPlotSettings} | ||||
|  |  | |||
|  | @ -37,10 +37,7 @@ 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"; | ||||
| import { defaultTickFormat } from "../lib/distributionSpecBuilder"; | ||||
| import { Button } from "./ui/Button"; | ||||
| 
 | ||||
| type PlaygroundProps = SquiggleChartProps & { | ||||
|  | @ -240,7 +237,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({ | |||
|   title, | ||||
|   minX, | ||||
|   maxX, | ||||
|   color = defaultColor, | ||||
|   tickFormat = defaultTickFormat, | ||||
|   distributionChartActions, | ||||
|   code: controlledCode, | ||||
|  | @ -268,7 +264,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({ | |||
|       title, | ||||
|       minX, | ||||
|       maxX, | ||||
|       color, | ||||
|       tickFormat, | ||||
|       distributionChartActions, | ||||
|       showSummary, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import React from "react"; | ||||
| import { squiggleExpression, declaration } from "@quri/squiggle-lang"; | ||||
| import { NumberShower } from "../NumberShower"; | ||||
| import { DistributionChart } from "../DistributionChart"; | ||||
| import { DistributionChart, defaultPlot, makePlot } from "../DistributionChart"; | ||||
| import { FunctionChart, FunctionChartSettings } from "../FunctionChart"; | ||||
| import clsx from "clsx"; | ||||
| import { VariableBox } from "./VariableBox"; | ||||
|  | @ -102,7 +102,7 @@ export const ExpressionViewer: React.FC<Props> = ({ | |||
|           {(settings) => { | ||||
|             return ( | ||||
|               <DistributionChart | ||||
|                 distribution={expression.value} | ||||
|                 plot={defaultPlot(expression.value)} | ||||
|                 {...settings.distributionPlotSettings} | ||||
|                 height={settings.height} | ||||
|                 width={width} | ||||
|  | @ -241,9 +241,9 @@ export const ExpressionViewer: React.FC<Props> = ({ | |||
|     case "module": { | ||||
|       return ( | ||||
|         <VariableList path={path} heading="Module"> | ||||
|           {(settings) => | ||||
|           {(_) => | ||||
|             Object.entries(expression.value) | ||||
|               .filter(([key, r]) => !key.match(/^(Math|System)\./)) | ||||
|               .filter(([key, _]) => !key.match(/^(Math|System)\./)) | ||||
|               .map(([key, r]) => ( | ||||
|                 <ExpressionViewer | ||||
|                   key={key} | ||||
|  | @ -257,9 +257,45 @@ export const ExpressionViewer: React.FC<Props> = ({ | |||
|       ); | ||||
|     } | ||||
|     case "record": | ||||
|       const plot = makePlot(expression.value); | ||||
|       if (plot) { | ||||
|         return ( | ||||
|           <VariableBox | ||||
|             path={path} | ||||
|             heading={"Plot"} | ||||
|             renderSettingsMenu={({ onChange }) => { | ||||
|               let disableLogX = plot.distributions.some((x) => { | ||||
|                 let pointSet = x.distribution.pointSet(); | ||||
|                 return ( | ||||
|                   pointSet.tag === "Ok" && hasMassBelowZero(pointSet.value) | ||||
|                 ); | ||||
|               }); | ||||
|               return ( | ||||
|                 <ItemSettingsMenu | ||||
|                   path={path} | ||||
|                   onChange={onChange} | ||||
|                   disableLogX={disableLogX} | ||||
|                   withFunctionSettings={false} | ||||
|                 /> | ||||
|               ); | ||||
|             }} | ||||
|           > | ||||
|             {(settings) => { | ||||
|               return ( | ||||
|                 <DistributionChart | ||||
|                   plot={plot} | ||||
|                   {...settings.distributionPlotSettings} | ||||
|                   height={settings.height} | ||||
|                   width={width} | ||||
|                 /> | ||||
|               ); | ||||
|             }} | ||||
|           </VariableBox> | ||||
|         ); | ||||
|       } else { | ||||
|         return ( | ||||
|           <VariableList path={path} heading="Record"> | ||||
|           {(settings) => | ||||
|             {(_) => | ||||
|               Object.entries(expression.value).map(([key, r]) => ( | ||||
|                 <ExpressionViewer | ||||
|                   key={key} | ||||
|  | @ -271,10 +307,11 @@ export const ExpressionViewer: React.FC<Props> = ({ | |||
|             } | ||||
|           </VariableList> | ||||
|         ); | ||||
|       } | ||||
|     case "array": | ||||
|       return ( | ||||
|         <VariableList path={path} heading="Array"> | ||||
|           {(settings) => | ||||
|           {(_) => | ||||
|             expression.value.map((r, i) => ( | ||||
|               <ExpressionViewer | ||||
|                 key={i} | ||||
|  |  | |||
|  | @ -6,10 +6,7 @@ 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"; | ||||
| import { defaultTickFormat } from "../../lib/distributionSpecBuilder"; | ||||
| import { PlaygroundContext } from "../SquigglePlayground"; | ||||
| 
 | ||||
| type Props = { | ||||
|  | @ -46,7 +43,6 @@ const ItemSettingsModal: React.FC< | |||
|       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, | ||||
|  | @ -66,7 +62,6 @@ const ItemSettingsModal: React.FC< | |||
|           expY: vars.expY, | ||||
|           format: vars.tickFormat, | ||||
|           title: vars.title, | ||||
|           color: vars.color, | ||||
|           minX: vars.minX, | ||||
|           maxX: vars.maxX, | ||||
|           actions: vars.distributionChartActions, | ||||
|  |  | |||
|  | @ -5,10 +5,7 @@ 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"; | ||||
| import { defaultTickFormat } from "../lib/distributionSpecBuilder"; | ||||
| 
 | ||||
| export const viewSettingsSchema = yup.object({}).shape({ | ||||
|   chartHeight: yup.number().required().positive().integer().default(350), | ||||
|  | @ -18,7 +15,6 @@ export const viewSettingsSchema = yup.object({}).shape({ | |||
|   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(), | ||||
|  | @ -114,12 +110,6 @@ export const ViewSettings: React.FC<{ | |||
|               register={register} | ||||
|               label="Tick Format" | ||||
|             /> | ||||
|             <InputItem | ||||
|               name="color" | ||||
|               type="color" | ||||
|               register={register} | ||||
|               label="Color" | ||||
|             /> | ||||
|           </div> | ||||
|         </HeadedSection> | ||||
|       </div> | ||||
|  |  | |||
|  | @ -10,8 +10,6 @@ export type DistributionChartSpecOptions = { | |||
|   minX?: number; | ||||
|   /** The maximum x coordinate shown on the chart */ | ||||
|   maxX?: number; | ||||
|   /** The color of the chart */ | ||||
|   color?: string; | ||||
|   /** The title of the chart */ | ||||
|   title?: string; | ||||
|   /** The formatting of the ticks */ | ||||
|  | @ -25,36 +23,14 @@ export let linearXScale: LinearScale = { | |||
|   range: "width", | ||||
|   zero: false, | ||||
|   nice: false, | ||||
|   domain: { | ||||
|     fields: [ | ||||
|       { | ||||
|         data: "con", | ||||
|         field: "x", | ||||
|       }, | ||||
|       { | ||||
|         data: "dis", | ||||
|         field: "x", | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   domain: { data: "domain", field: "x" }, | ||||
| }; | ||||
| export let linearYScale: LinearScale = { | ||||
|   name: "yscale", | ||||
|   type: "linear", | ||||
|   range: "height", | ||||
|   zero: true, | ||||
|   domain: { | ||||
|     fields: [ | ||||
|       { | ||||
|         data: "con", | ||||
|         field: "y", | ||||
|       }, | ||||
|       { | ||||
|         data: "dis", | ||||
|         field: "y", | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   domain: { data: "domain", field: "y" }, | ||||
| }; | ||||
| 
 | ||||
| export let logXScale: LogScale = { | ||||
|  | @ -65,18 +41,7 @@ export let logXScale: LogScale = { | |||
|   base: 10, | ||||
|   nice: false, | ||||
|   clamp: true, | ||||
|   domain: { | ||||
|     fields: [ | ||||
|       { | ||||
|         data: "con", | ||||
|         field: "x", | ||||
|       }, | ||||
|       { | ||||
|         data: "dis", | ||||
|         field: "x", | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   domain: { data: "domain", field: "x" }, | ||||
| }; | ||||
| 
 | ||||
| export let expYScale: PowScale = { | ||||
|  | @ -86,29 +51,16 @@ export let expYScale: PowScale = { | |||
|   range: "height", | ||||
|   zero: true, | ||||
|   nice: false, | ||||
|   domain: { | ||||
|     fields: [ | ||||
|       { | ||||
|         data: "con", | ||||
|         field: "y", | ||||
|       }, | ||||
|       { | ||||
|         data: "dis", | ||||
|         field: "y", | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   domain: { data: "domain", field: "y" }, | ||||
| }; | ||||
| 
 | ||||
| export const defaultTickFormat = ".9~s"; | ||||
| export const defaultColor = "#739ECC"; | ||||
| 
 | ||||
| export function buildVegaSpec( | ||||
|   specOptions: DistributionChartSpecOptions | ||||
| ): VisualizationSpec { | ||||
|   let { | ||||
|   const { | ||||
|     format = defaultTickFormat, | ||||
|     color = defaultColor, | ||||
|     title, | ||||
|     minX, | ||||
|     maxX, | ||||
|  | @ -127,20 +79,32 @@ export function buildVegaSpec( | |||
| 
 | ||||
|   let spec: VisualizationSpec = { | ||||
|     $schema: "https://vega.github.io/schema/vega/v5.json", | ||||
|     description: "A basic area chart example", | ||||
|     description: "Squiggle plot chart", | ||||
|     width: 500, | ||||
|     height: 100, | ||||
|     padding: 5, | ||||
|     data: [ | ||||
|       { | ||||
|         name: "con", | ||||
|         name: "data", | ||||
|       }, | ||||
|       { | ||||
|         name: "dis", | ||||
|         name: "domain", | ||||
|       }, | ||||
|     ], | ||||
|     signals: [], | ||||
|     scales: [xScale, expY ? expYScale : linearYScale], | ||||
|     scales: [ | ||||
|       xScale, | ||||
|       expY ? expYScale : linearYScale, | ||||
|       { | ||||
|         name: "color", | ||||
|         type: "ordinal", | ||||
|         domain: { | ||||
|           data: "data", | ||||
|           field: "name", | ||||
|         }, | ||||
|         range: { scheme: "blues" }, | ||||
|       }, | ||||
|     ], | ||||
|     axes: [ | ||||
|       { | ||||
|         orient: "bottom", | ||||
|  | @ -152,13 +116,40 @@ export function buildVegaSpec( | |||
|         domainOpacity: 0.0, | ||||
|         format: format, | ||||
|         tickCount: 10, | ||||
|         labelOverlap: "greedy", | ||||
|       }, | ||||
|     ], | ||||
|     marks: [ | ||||
|       { | ||||
|         name: "all_distributions", | ||||
|         type: "group", | ||||
|         from: { | ||||
|           facet: { | ||||
|             name: "distribution_facet", | ||||
|             data: "data", | ||||
|             groupby: ["name"], | ||||
|           }, | ||||
|         }, | ||||
|         marks: [ | ||||
|           { | ||||
|             name: "continuous_distribution", | ||||
|             type: "group", | ||||
|             from: { | ||||
|               facet: { | ||||
|                 name: "continuous_facet", | ||||
|                 data: "distribution_facet", | ||||
|                 field: "continuous", | ||||
|               }, | ||||
|             }, | ||||
|             encode: { | ||||
|               update: {}, | ||||
|             }, | ||||
|             marks: [ | ||||
|               { | ||||
|                 name: "continuous_area", | ||||
|                 type: "area", | ||||
|                 from: { | ||||
|           data: "con", | ||||
|                   data: "continuous_facet", | ||||
|                 }, | ||||
|                 encode: { | ||||
|                   update: { | ||||
|  | @ -171,23 +162,37 @@ export function buildVegaSpec( | |||
|                       scale: "yscale", | ||||
|                       field: "y", | ||||
|                     }, | ||||
|                     fill: { | ||||
|                       scale: "color", | ||||
|                       field: { parent: "name" }, | ||||
|                     }, | ||||
|                     y2: { | ||||
|                       scale: "yscale", | ||||
|                       value: 0, | ||||
|                     }, | ||||
|             fill: { | ||||
|               value: color, | ||||
|             }, | ||||
|                     fillOpacity: { | ||||
|                       value: 1, | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|           { | ||||
|             name: "discrete_distribution", | ||||
|             type: "group", | ||||
|             from: { | ||||
|               facet: { | ||||
|                 name: "discrete_facet", | ||||
|                 data: "distribution_facet", | ||||
|                 field: "discrete", | ||||
|               }, | ||||
|             }, | ||||
|             marks: [ | ||||
|               { | ||||
|                 type: "rect", | ||||
|                 from: { | ||||
|           data: "dis", | ||||
|                   data: "discrete_facet", | ||||
|                 }, | ||||
|                 encode: { | ||||
|                   enter: { | ||||
|  | @ -209,7 +214,8 @@ export function buildVegaSpec( | |||
|                       value: 0, | ||||
|                     }, | ||||
|                     fill: { | ||||
|               value: "#2f65a7", | ||||
|                       scale: "color", | ||||
|                       field: { parent: "name" }, | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|  | @ -217,7 +223,7 @@ export function buildVegaSpec( | |||
|               { | ||||
|                 type: "symbol", | ||||
|                 from: { | ||||
|           data: "dis", | ||||
|                   data: "discrete_facet", | ||||
|                 }, | ||||
|                 encode: { | ||||
|                   enter: { | ||||
|  | @ -239,21 +245,49 @@ export function buildVegaSpec( | |||
|                       field: "y", | ||||
|                     }, | ||||
|                     fill: { | ||||
|               value: "#1e4577", | ||||
|                       scale: "color", | ||||
|                       field: { parent: "name" }, | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             ], | ||||
|   }; | ||||
|   if (title) { | ||||
|     spec = { | ||||
|       ...spec, | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ], | ||||
|     legends: [ | ||||
|       { | ||||
|         fill: "color", | ||||
|         orient: "top", | ||||
|         labelFontSize: 12, | ||||
|         encode: { | ||||
|           symbols: { | ||||
|             update: { | ||||
|               fill: [ | ||||
|                 { test: "length(domain('color')) == 1", value: "transparent" }, | ||||
|                 { scale: "color", field: "value" }, | ||||
|               ], | ||||
|             }, | ||||
|           }, | ||||
|           labels: { | ||||
|             interactive: true, | ||||
|             update: { | ||||
|               fill: [ | ||||
|                 { test: "length(domain('color')) == 1", value: "transparent" }, | ||||
|                 { value: "black" }, | ||||
|               ], | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|     ...(title && { | ||||
|       title: { | ||||
|         text: title, | ||||
|       }, | ||||
|     }), | ||||
|   }; | ||||
|   } | ||||
| 
 | ||||
|   return spec; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										72
									
								
								packages/components/src/lib/plotParser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								packages/components/src/lib/plotParser.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | |||
| import * as yup from "yup"; | ||||
| import { Distribution, result, squiggleExpression } from "@quri/squiggle-lang"; | ||||
| 
 | ||||
| export type LabeledDistribution = { | ||||
|   name: string; | ||||
|   distribution: Distribution; | ||||
|   color?: string; | ||||
| }; | ||||
| 
 | ||||
| export type Plot = { | ||||
|   distributions: LabeledDistribution[]; | ||||
| }; | ||||
| 
 | ||||
| function error<a, b>(err: b): result<a, b> { | ||||
|   return { tag: "Error", value: err }; | ||||
| } | ||||
| 
 | ||||
| function ok<a, b>(x: a): result<a, b> { | ||||
|   return { tag: "Ok", value: x }; | ||||
| } | ||||
| 
 | ||||
| const schema = yup | ||||
|   .object() | ||||
|   .strict() | ||||
|   .noUnknown() | ||||
|   .shape({ | ||||
|     distributions: yup.object().shape({ | ||||
|       tag: yup.mixed().oneOf(["array"]), | ||||
|       value: yup | ||||
|         .array() | ||||
|         .of( | ||||
|           yup.object().shape({ | ||||
|             tag: yup.mixed().oneOf(["record"]), | ||||
|             value: yup.object({ | ||||
|               name: yup.object().shape({ | ||||
|                 tag: yup.mixed().oneOf(["string"]), | ||||
|                 value: yup.string().required(), | ||||
|               }), | ||||
|               // color: yup
 | ||||
|               //   .object({
 | ||||
|               //     tag: yup.mixed().oneOf(["string"]),
 | ||||
|               //     value: yup.string().required(),
 | ||||
|               //   })
 | ||||
|               //   .default(undefined),
 | ||||
|               distribution: yup.object({ | ||||
|                 tag: yup.mixed().oneOf(["distribution"]), | ||||
|                 value: yup.mixed(), | ||||
|               }), | ||||
|             }), | ||||
|           }) | ||||
|         ) | ||||
|         .required(), | ||||
|     }), | ||||
|   }); | ||||
| 
 | ||||
| export function parsePlot(record: { | ||||
|   [key: string]: squiggleExpression; | ||||
| }): result<Plot, string> { | ||||
|   try { | ||||
|     const plotRecord = schema.validateSync(record); | ||||
|     return ok({ | ||||
|       distributions: plotRecord.distributions.value.map((x) => ({ | ||||
|         name: x.value.name.value, | ||||
|         // color: x.value.color?.value, // not supported yet
 | ||||
|         distribution: x.value.distribution.value, | ||||
|       })), | ||||
|     }); | ||||
|   } catch (e) { | ||||
|     const message = e instanceof Error ? e.message : "Unknown error"; | ||||
|     return error(message); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										37
									
								
								packages/components/src/lib/utility.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/components/src/lib/utility.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| import { result } from "@quri/squiggle-lang"; | ||||
| 
 | ||||
| export function flattenResult<a, b>(x: result<a, b>[]): result<a[], b> { | ||||
|   if (x.length === 0) { | ||||
|     return { tag: "Ok", value: [] }; | ||||
|   } else { | ||||
|     if (x[0].tag === "Error") { | ||||
|       return x[0]; | ||||
|     } else { | ||||
|       let rest = flattenResult(x.splice(1)); | ||||
|       if (rest.tag === "Error") { | ||||
|         return rest; | ||||
|       } else { | ||||
|         return { tag: "Ok", value: [x[0].value].concat(rest.value) }; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function resultBind<a, b, c>( | ||||
|   x: result<a, b>, | ||||
|   fn: (y: a) => result<c, b> | ||||
| ): result<c, b> { | ||||
|   if (x.tag === "Ok") { | ||||
|     return fn(x.value); | ||||
|   } else { | ||||
|     return x; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function all(arr: boolean[]): boolean { | ||||
|   return arr.reduce((x, y) => x && y, true); | ||||
| } | ||||
| 
 | ||||
| export function some(arr: boolean[]): boolean { | ||||
|   return arr.reduce((x, y) => x || y, false); | ||||
| } | ||||
|  | @ -93,6 +93,33 @@ could be continuous, discrete or mixed. | |||
|   </Story> | ||||
| </Canvas> | ||||
| 
 | ||||
| ## Multiple plots | ||||
| 
 | ||||
| <Canvas> | ||||
|   <Story | ||||
|     name="Multiple plots" | ||||
|     args={{ | ||||
|       code: ` | ||||
| { | ||||
|   distributions: [ | ||||
|     { | ||||
|       name: "one", | ||||
|       distribution: mx(0.5, normal(0,1)) | ||||
|     }, | ||||
|     { | ||||
|       name: "two", | ||||
|       distribution: mx(2, normal(5, 2)), | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| `, | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|   </Story> | ||||
| </Canvas> | ||||
| 
 | ||||
| ## Constants | ||||
| 
 | ||||
| A constant is a simple number as a result. This has special formatting rules | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
|     "@docusaurus/core": "2.0.1", | ||||
|     "@docusaurus/preset-classic": "2.0.1", | ||||
|     "@heroicons/react": "^1.0.6", | ||||
|     "@quri/squiggle-components": "^0.2.23", | ||||
|     "@quri/squiggle-components": "^0.3", | ||||
|     "base64-js": "^1.5.1", | ||||
|     "clsx": "^1.2.1", | ||||
|     "hast-util-is-element": "2.1.2", | ||||
|  |  | |||
							
								
								
									
										122
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								yarn.lock
									
									
									
									
									
								
							|  | @ -2188,23 +2188,11 @@ | |||
|     minimatch "^3.1.2" | ||||
|     strip-json-comments "^3.1.1" | ||||
| 
 | ||||
| "@floating-ui/core@^0.7.3": | ||||
|   version "0.7.3" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86" | ||||
|   integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg== | ||||
| 
 | ||||
| "@floating-ui/core@^1.0.0": | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.0.tgz#ec1d31f54c72dd0460276e2149e59bd13c0f01f6" | ||||
|   integrity sha512-sm3nW0hHAxTv3gRDdCH8rNVQxijF+qPFo5gAeXCErRjKC7Qc28lIQ3R9Vd7Gw+KgwfA7RhRydDFuGeI0peGq7A== | ||||
| 
 | ||||
| "@floating-ui/dom@^0.5.3": | ||||
|   version "0.5.4" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.4.tgz#4eae73f78bcd4bd553ae2ade30e6f1f9c73fe3f1" | ||||
|   integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg== | ||||
|   dependencies: | ||||
|     "@floating-ui/core" "^0.7.3" | ||||
| 
 | ||||
| "@floating-ui/dom@^1.0.0": | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.0.tgz#66923a56755b6cb7a5958ecf25fe293912672d65" | ||||
|  | @ -2212,15 +2200,6 @@ | |||
|   dependencies: | ||||
|     "@floating-ui/core" "^1.0.0" | ||||
| 
 | ||||
| "@floating-ui/react-dom-interactions@^0.6.6": | ||||
|   version "0.6.6" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.6.tgz#8542e8c4bcbee2cd0d512de676c6a493e0a2d168" | ||||
|   integrity sha512-qnao6UPjSZNHnXrF+u4/n92qVroQkx0Umlhy3Avk1oIebm/5ee6yvDm4xbHob0OjY7ya8WmUnV3rQlPwX3Atwg== | ||||
|   dependencies: | ||||
|     "@floating-ui/react-dom" "^0.7.2" | ||||
|     aria-hidden "^1.1.3" | ||||
|     use-isomorphic-layout-effect "^1.1.1" | ||||
| 
 | ||||
| "@floating-ui/react-dom-interactions@^0.9.2": | ||||
|   version "0.9.2" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.9.2.tgz#9a364cc44ecbc242b5218dff0e0d071de115e13a" | ||||
|  | @ -2229,14 +2208,6 @@ | |||
|     "@floating-ui/react-dom" "^1.0.0" | ||||
|     aria-hidden "^1.1.3" | ||||
| 
 | ||||
| "@floating-ui/react-dom@^0.7.2": | ||||
|   version "0.7.2" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864" | ||||
|   integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg== | ||||
|   dependencies: | ||||
|     "@floating-ui/dom" "^0.5.3" | ||||
|     use-isomorphic-layout-effect "^1.1.1" | ||||
| 
 | ||||
| "@floating-ui/react-dom@^1.0.0": | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.0.0.tgz#e0975966694433f1f0abffeee5d8e6bb69b7d16e" | ||||
|  | @ -2279,7 +2250,7 @@ | |||
|   resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324" | ||||
|   integrity sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ== | ||||
| 
 | ||||
| "@hookform/resolvers@^2.9.6", "@hookform/resolvers@^2.9.7": | ||||
| "@hookform/resolvers@^2.9.7": | ||||
|   version "2.9.7" | ||||
|   resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.9.7.tgz#8b257ae67234ce0270e6b044c1a61fb98ec02b4b" | ||||
|   integrity sha512-BloehX3MOLwuFEwT4yZnmolPjVmqyn8VsSuodLfazbCIqxBHsQ4qUZsi+bvNNCduRli1AGWFrkDLGD5QoNzsoA== | ||||
|  | @ -2683,7 +2654,7 @@ | |||
|   resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" | ||||
|   integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== | ||||
| 
 | ||||
| "@motionone/animation@^10.12.0", "@motionone/animation@^10.13.1": | ||||
| "@motionone/animation@^10.13.1": | ||||
|   version "10.13.2" | ||||
|   resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.13.2.tgz#174a55a3bac1b6fb314cc1c3627093dc790ae081" | ||||
|   integrity sha512-YGWss58IR2X4lOjW89rv1Q+/Nq/QhfltaggI7i8sZTpKC1yUvM+XYDdvlRpWc6dk8LviMBrddBJAlLdbaqeRmw== | ||||
|  | @ -2693,18 +2664,6 @@ | |||
|     "@motionone/utils" "^10.13.2" | ||||
|     tslib "^2.3.1" | ||||
| 
 | ||||
| "@motionone/dom@10.12.0": | ||||
|   version "10.12.0" | ||||
|   resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.12.0.tgz#ae30827fd53219efca4e1150a5ff2165c28351ed" | ||||
|   integrity sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw== | ||||
|   dependencies: | ||||
|     "@motionone/animation" "^10.12.0" | ||||
|     "@motionone/generators" "^10.12.0" | ||||
|     "@motionone/types" "^10.12.0" | ||||
|     "@motionone/utils" "^10.12.0" | ||||
|     hey-listen "^1.0.8" | ||||
|     tslib "^2.3.1" | ||||
| 
 | ||||
| "@motionone/dom@10.13.1": | ||||
|   version "10.13.1" | ||||
|   resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.13.1.tgz#fc29ea5d12538f21b211b3168e502cfc07a24882" | ||||
|  | @ -2725,7 +2684,7 @@ | |||
|     "@motionone/utils" "^10.13.2" | ||||
|     tslib "^2.3.1" | ||||
| 
 | ||||
| "@motionone/generators@^10.12.0", "@motionone/generators@^10.13.1": | ||||
| "@motionone/generators@^10.13.1": | ||||
|   version "10.13.2" | ||||
|   resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.13.2.tgz#dd972195b899e7a556d65bd27fae2fd423055e10" | ||||
|   integrity sha512-QMoXV1MXEEhR6D3dct/RMMS1FwJlAsW+kMPbFGzBA4NbweblgeYQCft9DcDAVpV9wIwD6qvlBG9u99sOXLfHiA== | ||||
|  | @ -2734,12 +2693,12 @@ | |||
|     "@motionone/utils" "^10.13.2" | ||||
|     tslib "^2.3.1" | ||||
| 
 | ||||
| "@motionone/types@^10.12.0", "@motionone/types@^10.13.0", "@motionone/types@^10.13.2": | ||||
| "@motionone/types@^10.13.0", "@motionone/types@^10.13.2": | ||||
|   version "10.13.2" | ||||
|   resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.13.2.tgz#c560090d81bd0149e7451aae23ab7af458570363" | ||||
|   integrity sha512-yYV4q5v5F0iADhab4wHfqaRJnM/eVtQLjUPhyEcS72aUz/xyOzi09GzD/Gu+K506BDfqn5eULIilUI77QNaqhw== | ||||
| 
 | ||||
| "@motionone/utils@^10.12.0", "@motionone/utils@^10.13.1", "@motionone/utils@^10.13.2": | ||||
| "@motionone/utils@^10.13.1", "@motionone/utils@^10.13.2": | ||||
|   version "10.13.2" | ||||
|   resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.13.2.tgz#ce79bfe1d133493c217cdc0584960434e065648d" | ||||
|   integrity sha512-6Lw5bDA/w7lrPmT/jYWQ76lkHlHs9fl2NZpJ22cVy1kKDdEH+Cl1U6hMTpdphO6VQktQ6v2APngag91WBKLqlA== | ||||
|  | @ -2818,33 +2777,7 @@ | |||
|   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" | ||||
|   integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== | ||||
| 
 | ||||
| "@quri/squiggle-components@^0.2.23": | ||||
|   version "0.2.24" | ||||
|   resolved "https://registry.yarnpkg.com/@quri/squiggle-components/-/squiggle-components-0.2.24.tgz#16a2d72fb16f46a0bf71388c85d1238927676923" | ||||
|   integrity sha512-slBGryELfCsM6WX+AwQcqiPPoImLRHNyXZDueL7a+OKEAx09w3pKOqVzLWNGL7+dJe3dF8as9X/Gv1JbbIj5yw== | ||||
|   dependencies: | ||||
|     "@floating-ui/react-dom" "^0.7.2" | ||||
|     "@floating-ui/react-dom-interactions" "^0.6.6" | ||||
|     "@headlessui/react" "^1.6.6" | ||||
|     "@heroicons/react" "^1.0.6" | ||||
|     "@hookform/resolvers" "^2.9.6" | ||||
|     "@quri/squiggle-lang" "^0.2.8" | ||||
|     "@react-hook/size" "^2.1.2" | ||||
|     clsx "^1.2.1" | ||||
|     framer-motion "^6.5.1" | ||||
|     lodash "^4.17.21" | ||||
|     react "^18.1.0" | ||||
|     react-ace "^10.1.0" | ||||
|     react-hook-form "^7.33.1" | ||||
|     react-use "^17.4.0" | ||||
|     react-vega "^7.6.0" | ||||
|     vega "^5.22.1" | ||||
|     vega-embed "^6.21.0" | ||||
|     vega-lite "^5.3.0" | ||||
|     vscode-uri "^3.0.3" | ||||
|     yup "^0.32.11" | ||||
| 
 | ||||
| "@quri/squiggle-lang@^0.2.11", "@quri/squiggle-lang@^0.2.8": | ||||
| "@quri/squiggle-lang@^0.2.11": | ||||
|   version "0.2.12" | ||||
|   resolved "https://registry.yarnpkg.com/@quri/squiggle-lang/-/squiggle-lang-0.2.12.tgz#e8fdb22a84aa75df71c071d1ed4ae5c55f15d447" | ||||
|   integrity sha512-fgv9DLvPlX/TqPSacKSW3GZ5S9H/YwqaMoRdFrn5SJjHnnMh/xJW/9iyzzgOxPCXov9xFeDvL159tkbStMm7vw== | ||||
|  | @ -9721,20 +9654,6 @@ fragment-cache@^0.2.1: | |||
|   dependencies: | ||||
|     map-cache "^0.2.2" | ||||
| 
 | ||||
| framer-motion@^6.5.1: | ||||
|   version "6.5.1" | ||||
|   resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7" | ||||
|   integrity sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw== | ||||
|   dependencies: | ||||
|     "@motionone/dom" "10.12.0" | ||||
|     framesync "6.0.1" | ||||
|     hey-listen "^1.0.8" | ||||
|     popmotion "11.0.3" | ||||
|     style-value-types "5.0.0" | ||||
|     tslib "^2.1.0" | ||||
|   optionalDependencies: | ||||
|     "@emotion/is-prop-valid" "^0.8.2" | ||||
| 
 | ||||
| framer-motion@^7.1.1: | ||||
|   version "7.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-7.1.1.tgz#4d56ed18a7cf2c6a1a4a1af5b57714f8e6b52d9e" | ||||
|  | @ -9749,13 +9668,6 @@ framer-motion@^7.1.1: | |||
|   optionalDependencies: | ||||
|     "@emotion/is-prop-valid" "^0.8.2" | ||||
| 
 | ||||
| framesync@6.0.1: | ||||
|   version "6.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20" | ||||
|   integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA== | ||||
|   dependencies: | ||||
|     tslib "^2.1.0" | ||||
| 
 | ||||
| framesync@6.1.2: | ||||
|   version "6.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.1.2.tgz#755eff2fb5b8f3b4d2b266dd18121b300aefea27" | ||||
|  | @ -13963,16 +13875,6 @@ polished@^4.2.2: | |||
|   dependencies: | ||||
|     "@babel/runtime" "^7.17.8" | ||||
| 
 | ||||
| popmotion@11.0.3: | ||||
|   version "11.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9" | ||||
|   integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA== | ||||
|   dependencies: | ||||
|     framesync "6.0.1" | ||||
|     hey-listen "^1.0.8" | ||||
|     style-value-types "5.0.0" | ||||
|     tslib "^2.1.0" | ||||
| 
 | ||||
| popmotion@11.0.5: | ||||
|   version "11.0.5" | ||||
|   resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.5.tgz#8e3e014421a0ffa30ecd722564fd2558954e1f7d" | ||||
|  | @ -15165,7 +15067,7 @@ react-helmet-async@*, react-helmet-async@^1.3.0: | |||
|     react-fast-compare "^3.2.0" | ||||
|     shallowequal "^1.1.0" | ||||
| 
 | ||||
| react-hook-form@^7.33.1, react-hook-form@^7.34.1: | ||||
| react-hook-form@^7.34.1: | ||||
|   version "7.34.1" | ||||
|   resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.34.1.tgz#06cb216daf706bf9ae4969747115afae0d09410d" | ||||
|   integrity sha512-tH7TaZgAURMhjzVE2M/EFmxHz2HdaPMAVs9FXTweNW551VlhXSuVcpcYlkiMZf2zHQiTztupVFpBHJFTma+N7w== | ||||
|  | @ -16972,14 +16874,6 @@ style-to-object@0.3.0, style-to-object@^0.3.0: | |||
|   dependencies: | ||||
|     inline-style-parser "0.1.1" | ||||
| 
 | ||||
| style-value-types@5.0.0: | ||||
|   version "5.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad" | ||||
|   integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA== | ||||
|   dependencies: | ||||
|     hey-listen "^1.0.8" | ||||
|     tslib "^2.1.0" | ||||
| 
 | ||||
| style-value-types@5.1.2: | ||||
|   version "5.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.1.2.tgz#6be66b237bd546048a764883528072ed95713b62" | ||||
|  | @ -18236,7 +18130,7 @@ vega-label@~1.2.0: | |||
|     vega-scenegraph "^4.9.2" | ||||
|     vega-util "^1.15.2" | ||||
| 
 | ||||
| vega-lite@^5.3.0, vega-lite@^5.4.0: | ||||
| vega-lite@^5.4.0: | ||||
|   version "5.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.4.0.tgz#d09331e2a1c87843d5865de0fa7704919796ab56" | ||||
|   integrity sha512-e/P5iOtBE62WEWZhKP7sLcBd92YS9prfUQafelxoOeloooSSrkUwM/ZDmN5Q5ffByEZTiKfODtnwD6/xKDYUmw== | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user