Compare commits

..

No commits in common. "develop" and "playground-as-own-page" have entirely different histories.

449 changed files with 41644 additions and 33053 deletions

25
.github/CODEOWNERS vendored
View File

@ -8,24 +8,15 @@
# IMPORTANT NOTE: in order to actually get pinged, commit access is required. # IMPORTANT NOTE: in order to actually get pinged, commit access is required.
# This also holds true for GitHub teams. # This also holds true for GitHub teams.
# Rescript # This file
*.res @berekuk @OAGr /.github/CODEOWNERS @quinn-dougherty
*.resi @berekuk @OAGr
# Typescript # Any rescript code
*.tsx @Hazelfire @berekuk @OAGr *.res @Hazelfire @OAGr @quinn-dougherty
*.ts @Hazelfire @berekuk @OAGr
# Javascript # Any typescript code
*.js @Hazelfire @berekuk @OAGr *.tsx @Hazelfire @OAGr
# Any opsy files # Any opsy files
.github/** @quinn-dougherty @berekuk @OAGr *.json @quinn-dougherty @Hazelfire
*.json @quinn-dougherty @Hazelfire @berekuk @OAGr *.y*ml @quinn-dougherty
*.y*ml @quinn-dougherty @berekuk @OAGr
*.config.js @Hazelfire @berekuk @OAGr
vercel.json @OAGr @berekuk @Hazelfire
# Documentation
*.md @quinn-dougherty @OAGr @Hazelfire
*.mdx @quinn-dougherty @OAGr @Hazelfire

View File

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Ideas and feature requests - Squiggle Discussions on GitHub
url: https://github.com/quantified-uncertainty/squiggle/discussions
about: Please propose and discuss new features here. Remember to search for your idea before posting a new topic! Where would you like to see Squiggle go over the next few months, several months, or few years?

14
.github/ISSUE_TEMPLATE/developer-bug.md vendored Normal file
View File

@ -0,0 +1,14 @@
---
name: Developer friction when contributing to Squiggle
about: Did your yarn scripts fail? Did the CI diverge from a README? Have a testing-related task? Etc.
labels: 'ops & testing'
---
# Description:
# The OS and version, yarn version, etc. in which this came up
_delete this section if testing task_
# Desired behavior

7
.github/ISSUE_TEMPLATE/future.md vendored Normal file
View File

@ -0,0 +1,7 @@
---
name: Idea or feature request
about: Where would you like to see Squiggle go over the next few months, several months, or few years?
---
# Description

View File

@ -1,13 +0,0 @@
---
name: Operations and testing
about: Have a testing-related task? Developer friction when contributing to squiggle? Etc.
labels: "ops & testing"
---
# Description:
# The OS and version, yarn version, etc. in which this came up
<!-- delete this section if testing task or otherwise not applicable -->
# Desired behavior

View File

@ -1,13 +1,14 @@
--- ---
name: Regarding the programming language (the `squiggle-lang` package) name: Regarding the programming language
about: Anything concerning distributions/numerics, as well as the interpreter, parser, syntax, semantics about: Interpreter, parser, syntax, semantics, and including distributions
labels: "programming language" labels: 'programming language'
--- ---
<!-- mark one with an x --> <!-- mark one with an x -->
- _ Is refactor
- \_ Is refactor - _ Is new feature
- \_ Is new feature - _ Concerns documentation
- \_ Concerns documentation
# Description of suggestion or shortcoming: # Description of suggestion or shortcoming:

View File

@ -1,17 +1,18 @@
--- ---
name: Bug reports for Squiggle users name: Bug reports for Squiggle users
about: Rendering oddly? Is there a mathematical correctness problem? about: Rendering oddly, trouble with the playground, something like this?
labels: "bug" labels: 'bug'
--- ---
# Description: # Description:
# Steps to reproduce:
# Steps to reproduce:
1. 1.
2. 2.
3. 3.
# Expected behavior: # Expected behavior:
# What I got instead: # What I got instead:

View File

@ -8,17 +8,4 @@ updates:
- package-ecosystem: "npm" # See documentation for possible values - package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "daily"
commit-message:
prefix: "⬆️"
open-pull-requests-limit: 100
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "⬆️"
labels:
- "dependencies"

View File

@ -1,87 +0,0 @@
name: Nix build
on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
- reducer-dev
- epic-reducer-project
jobs:
flake-lints:
name: All lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
with:
nix_path: nixpkgs=channel:nixos-22.05
- name: Use cachix
uses: cachix/cachix-action@v10
with:
name: quantified-uncertainty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Check that lang lints
run: nix build .#lang-lint
- name: Check that components lints
run: nix build .#components-lint
- name: Check that website lints
run: nix build .#docusaurus-lint
- name: Check that vscode extension lints
run: nix build .#vscode-lint
- name: Check that cli lints
run: nix build .#cli-lint
flake-packages:
name: Builds, tests, and bundles
runs-on: ubuntu-latest
needs: flake-lints
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
with:
nix_path: nixpkgs=channel:nixos-22.05
- name: Use cachix
uses: cachix/cachix-action@v10
with:
name: quantified-uncertainty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Check all lang tests
run: nix build .#lang-test
- name: Check that lang bundles
run: nix build .#lang-bundle
- name: Check that components builds
run: nix build .#components
- name: Check that components bundles
run: nix build .#components-bundle
flake-devshells:
name: Development shell environment
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
with:
nix_path: nixpkgs=channel:nixos-22.05
- name: Use cachix
uses: cachix/cachix-action@v10
with:
name: quantified-uncertainty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build js devshell
run: nix develop .#js --profile just-js
- name: Build js & wasm devshell
run: nix develop --profile full-shell

92
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,92 @@
name: Squiggle packages check
on:
push: # Delete this line if there becomes a scarcity of build minutes.
pull_request:
branches:
- master
- staging
jobs:
pre_check:
name: Precheck for skipping redundant jobs
runs-on: ubuntu-latest
outputs:
should_skip_lang: ${{ steps.skip_lang_check.outputs.should_skip }}
should_skip_components: ${{ steps.skip_components_check.outputs.should_skip }}
should_skip_website: ${{ steps.skip_website_check.outputs.should_skip }}
steps:
- id: skip_lang_check
name: Check if the changes are about squiggle-lang src files
uses: fkirc/skip-duplicate-actions@master
with:
paths: '["packages/squiggle-lang/**"]'
- id: skip_components_check
name: Check if the changes are about components src files
uses: fkirc/skip-duplicate-actions@master
with:
paths: '["packages/components/**"]'
- id: skip_website_check
name: Check if the changes are about website src files
uses: fkirc/skip-duplicate-actions@master
with:
paths: '["packages/website/**"]'
lang-build-test:
name: Language build and test
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_lang != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/squiggle-lang
steps:
- uses: actions/checkout@v2
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build rescript codebase
run: yarn build
- name: Run tests
run: yarn test
- name: Run webpack
run: yarn bundle
components-build-test:
name: Components build and test
runs-on: ubuntu-latest
needs: [pre_check]
if: ${{ needs.pre_check.outputs.should_skip_components != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/components
steps:
- uses: actions/checkout@v2
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build rescript codebase in squiggle-lang
run: cd ../squiggle-lang && yarn build
- name: Run webpack
run: yarn bundle
- name: Build storybook
run: yarn build
website-build:
name: Website build
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_website != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/website
steps:
- uses: actions/checkout@v2
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build rescript in squiggle-lang
run: cd ../squiggle-lang && yarn build
- name: Build website assets
run: yarn build

View File

@ -1,48 +0,0 @@
name: Squiggle packages checks
on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: quantified-uncertainty
jobs:
build-test-lint:
name: Build, test, lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Turbo run
run: npx turbo run build test lint bundle
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Coverage
run: npx turbo run coverage

View File

@ -12,8 +12,17 @@
name: "CodeQL" name: "CodeQL"
on: on:
push:
branches:
- master
- staging
pull_request:
# The branches below must be a subset of the branches above
branches:
- master
- staging
schedule: schedule:
- cron: "42 19 * * 0" - cron: '42 19 * * 0'
jobs: jobs:
analyze: analyze:
@ -27,42 +36,39 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ["javascript"] language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support # Learn more about CodeQL language support at https://git.io/codeql-language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v1
- name: Install dependencies
run: yarn
- name: Build rescript
run: cd packages/squiggle-lang && yarn build
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # Command-line programs to run using the OS shell.
# and modify them (or add more) to build your code if your project # 📚 https://git.io/JvXDl
# uses a compiled language
#- run: | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# make bootstrap # and modify them (or add more) to build your code if your project
# make release # uses a compiled language
- name: Perform CodeQL Analysis #- run: |
uses: github/codeql-action/analyze@v2 # make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,141 +0,0 @@
name: Run Release Please
on:
push:
branches:
- master
jobs:
pre_check:
name: Precheck for skipping redundant jobs
runs-on: ubuntu-latest
outputs:
should_skip_lang: ${{ steps.skip_lang_check.outputs.should_skip }}
should_skip_components: ${{ steps.skip_components_check.outputs.should_skip }}
should_skip_website: ${{ steps.skip_website_check.outputs.should_skip }}
should_skip_vscodeext: ${{ steps.skip_vscodeext_check.outputs.should_skip }}
should_skip_cli: ${{ steps.skip_cli_check.outputs.should_skip }}
steps:
- id: skip_lang_check
name: Check if the changes are about squiggle-lang src files
uses: fkirc/skip-duplicate-actions@v5.2.0
with:
paths: '["packages/squiggle-lang/**"]'
- id: skip_components_check
name: Check if the changes are about components src files
uses: fkirc/skip-duplicate-actions@v5.2.0
with:
paths: '["packages/components/**"]'
- id: skip_website_check
name: Check if the changes are about website src files
uses: fkirc/skip-duplicate-actions@v5.2.0
with:
paths: '["packages/website/**"]'
- id: skip_vscodeext_check
name: Check if the changes are about vscode extension src files
uses: fkirc/skip-duplicate-actions@v5.2.0
with:
paths: '["packages/vscode-ext/**"]'
- id: skip_cli_check
name: Check if the changes are about cli src files
uses: fkirc/skip-duplicate-actions@v5.2.0
with:
paths: '["packages/cli/**"]'
relplz-lang:
name: for squiggle-lang
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_lang != 'true' }}
steps:
- name: Release please (squiggle-lang)
uses: google-github-actions/release-please-action@v3
id: release
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/squiggle-lang
# bump-patch-for-minor-pre-major: true
skip-github-release: true
- name: Publish- Checkout source
uses: actions/checkout@v3
# these if statements ensure that a publication only occurs when
# a new release is created:
if: ${{ steps.release.outputs.release_created }}
- name: Publish- Install dependencies
run: yarn
if: ${{ steps.release.outputs.release_created }}
- name: Publish
run: cd packages/squiggle-lang && yarn publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: ${{ steps.release.outputs.release_created }}
relplz-components:
name: for components
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_components != 'true' }}
steps:
- name: Release please (components)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/components
# bump-patch-for-minor-pre-major: true
skip-github-release: true
- name: Publish- Checkout source
uses: actions/checkout@v3
# these if statements ensure that a publication only occurs when
# a new release is created:
if: ${{ steps.release.outputs.release_created }}
- name: Publish- Install dependencies
run: yarn
if: ${{ steps.release.outputs.release_created }}
- name: Publish
run: cd packages/components && yarn publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
relplz-website:
name: for website
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_website != 'true' }}
steps:
- name: Release please (website)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/website
# bump-patch-for-minor-pre-major: true
skip-github-release: true
relplz-vscodeext:
name: for vscode-ext
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_vscodeext != 'true' }}
steps:
- name: Release please (vscode-ext)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/vscode-ext
# bump-patch-for-minor-pre-major: true
skip-github-release: true
relplz-cl:
name: for cli
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_cli != 'true' }}
steps:
- name: Release please (cli)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/cli
bump-patch-for-minor-pre-major: true
skip-github-release: true

7
.gitignore vendored
View File

@ -6,10 +6,3 @@ yarn-error.log
.DS_Store .DS_Store
**/.sync.ffs_db **/.sync.ffs_db
.direnv .direnv
.log
.vscode
todo.txt
result
shell.nix
.turbo

6
.parcelrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.res": ["@parcel/transformer-raw"]
}
}

View File

@ -1,16 +0,0 @@
.direnv
*.bs.js
*.gen.tsx
packages/components/storybook-static
node_modules
packages/*/node_modules
packages/website/.docusaurus
packages/squiggle-lang/lib
packages/squiggle-lang/coverage/
packages/squiggle-lang/.cache/
packages/website/build/
packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
packages/vscode-ext/media/vendor/
packages/squiggle-lang/.nyc_output/
packages/*/dist
result

View File

@ -1,7 +0,0 @@
{
"packages/cli": "0.0.3",
"packages/components": "0.4.1",
"packages/squiggle-lang": "0.4.1",
"packages/vscode-ext": "0.4.1",
"packages/website": "0.0.0"
}

View File

@ -1 +0,0 @@
See the [Changelog.mdx page](./packages/website/docs/Changelog.mdx) for the changelog.

View File

@ -8,7 +8,7 @@ Squiggle is currently pre-alpha.
# Quick links # Quick links
- [Roadmap to the alpha](https://github.com/orgs/quantified-uncertainty/projects/1) - [Roadmap to the alpha](https://github.com/QURIresearch/squiggle/projects/2)
- The team presently communicates via the **EA Forecasting and Epistemics** slack (channels `#squiggle` and `#squiggle-ops`), you can track down an invite by reaching out to Ozzie Gooen - The team presently communicates via the **EA Forecasting and Epistemics** slack (channels `#squiggle` and `#squiggle-ops`), you can track down an invite by reaching out to Ozzie Gooen
- [Squiggle documentation](https://www.squiggle-language.com/docs/Language) - [Squiggle documentation](https://www.squiggle-language.com/docs/Language)
- [Rescript documentation](https://rescript-lang.org/docs/manual/latest/introduction) - [Rescript documentation](https://rescript-lang.org/docs/manual/latest/introduction)
@ -16,19 +16,19 @@ Squiggle is currently pre-alpha.
# Bug reports # Bug reports
Anyone (with a github account) can file an issue at any time. Please allow Slava, Sam, and Ozzie to triage, but otherwise just follow the suggestions in the issue templates. Anyone (with a github account) can file an issue at any time. Please allow Quinn, Sam, and Ozzie to triage, but otherwise just follow the suggestions in the issue templates.
# Project structure # Project structure
Squiggle is a **monorepo** with three **packages**. Squiggle is a **monorepo** with four **packages**.
- **components** is where we improve reactive interfacing with Squiggle - **components** is where we improve reactive interfacing with Squiggle
- **playground** is the site `playground.squiggle-language.com`
- **squiggle-lang** is where the magic happens: probability distributions, the interpreter, etc. - **squiggle-lang** is where the magic happens: probability distributions, the interpreter, etc.
- **website** is the site `squiggle-language.com` - **website** is the site `squiggle-language.com`
# Deployment ops # Deployment ops
We use Vercel, and it should only concern Slava, Sam, and Ozzie. We use netlify, and it should only concern Quinn, Sam, and Ozzie.
# Development environment, building, testing, dev server # Development environment, building, testing, dev server
@ -40,105 +40,13 @@ We aspire for `ci.yaml` and `README.md`s to be in one-to-one correspondence.
## If you're on NixOS ## If you're on NixOS
You can't run `yarn` outside of a FHS shell. Additionally, you need to `patchelf` some things. A script does everything for you. You'll need to run a command like this in order to get `yarn build` to run, especially in `packages/squiggle-lang`.
```sh ```sh
./nixos.sh patchelf --set-interpreter $(patchelf --print-interpreter $(which mkdir)) ./node_modules/gentype/gentype.exe
``` ```
Reasons for this are comments in the script. Then, you should be able to do all the package-level `yarn run` commands/scripts. See [here](https://github.com/NixOS/nixpkgs/issues/107375)
# Try not to push directly to develop
If you absolutely must, please prefix your commit message with `hotfix: `.
# Pull request protocol # Pull request protocol
Please work against `develop` branch. **Do not** work against `master`. Please work against `staging` branch. **Do not** work against `master`. Please do not merge without approval from some subset of Quinn, Sam, and Ozzie; they will be auto-pinged.
- For rescript code: Slava and Ozzie are reviewers
- For js or typescript code: Sam and Ozzie are reviewers
- For ops code (i.e. yaml, package.json): Slava and Sam are reviewers
Autopings are set up: if you are not autopinged, you are welcome to comment, but please do not use the formal review feature, send approvals, rejections, or merges.
# Code Quality
- Aim for at least 8/10\* quality in `/packages/squiggle-lang`, and 7/10 quality in `/packages/components`.
- If you submit a PR that is under a 7, for some reason, describe the reasoning for this in the PR.
* This quality score is subjective.
# Rescript Style
**Use `->` instead of `|>`**
Note: Our codebase used to use `|>`, so there's a lot of that in the system. We'll gradually change it.
**Use `x -> y -> z` instead of `let foo = y(x); let bar = z(foo)`**
**Don't use anonymous functions with over three lines**
Bad:
```rescript
foo
-> E.O.fmap(r => {
let a = 34;
let b = 35;
let c = 48;
r + a + b + c
}
```
Good:
```rescript
let addingFn = (r => {
let a = 34;
let b = 35;
let c = 48;
r + a + b + c
}
foo -> addingFn
```
**Write out types for everything, even if there's an interface file**
We'll try this for one month (ending May 5, 2022), then revisit.
**Use the Rescript optional default syntax**
Rescript is clever about function inputs. There's custom syntax for default and optional arguments. In the cases where this applies, use it.
From https://rescript-lang.org/docs/manual/latest/function:
```rescript
// radius can be omitted
let drawCircle = (~color, ~radius=?, ()) => {
setColor(color)
switch radius {
| None => startAt(1, 1)
| Some(r_) => startAt(r_, r_)
}
}
```
**Use named arguments**
If a function is called externally (in a different file), and has either:
1. Two arguments of the same type
2. Three paramaters or more.
**Module naming: Use x_y as module names**
For example: `Myname_Myproject_Add.res`. Rescript/Ocaml both require files to have unique names, so long names are needed to keep different parts separate from each other.
See [this page](https://dev.to/yawaramin/a-modular-ocaml-project-structure-1ikd) for more information. (Though note that they use two underscores, and we do one. We might refactor that later.
**Module naming: Don't rename modules**
We have some of this in the Reducer code, but generally discourage it.
**Use interface files (.resi) for files with very public interfaces**
### Recommended Rescript resources
- https://dev.to/yawaramin/a-modular-ocaml-project-structure-1ikd
- https://github.com/avohq/reasonml-code-style-guide
- https://cs.brown.edu/courses/cs017/content/docs/reasonml-style.pdf
- https://github.com/ostera/reason-design-patterns/

View File

@ -1,76 +1,51 @@
# Squiggle # Squiggle
![Packages check](https://github.com/QURIresearch/squiggle/actions/workflows/ci.yaml/badge.svg)
[![npm version](https://badge.fury.io/js/@quri%2Fsquiggle-lang.svg)](https://www.npmjs.com/package/@quri/squiggle-lang)
[![npm version](https://badge.fury.io/js/@quri%2Fsquiggle-components.svg)](https://www.npmjs.com/package/@quri/squiggle-components)
[![Packages check](https://github.com/quantified-uncertainty/squiggle/actions/workflows/ci.yml/badge.svg)](https://github.com/quantified-uncertainty/squiggle/actions/workflows/ci.yml) This is an experimental DSL/language for making probabilistic estimates. The full story can be found [here](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3).
[![npm version - lang](https://badge.fury.io/js/@quri%2Fsquiggle-lang.svg)](https://www.npmjs.com/package/@quri/squiggle-lang)
[![npm version - components](https://badge.fury.io/js/@quri%2Fsquiggle-components.svg)](https://www.npmjs.com/package/@quri/squiggle-components)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/quantified-uncertainty/squiggle/blob/develop/LICENSE)
[![codecov](https://codecov.io/gh/quantified-uncertainty/squiggle/branch/develop/graph/badge.svg?token=QRLBL5CQ7C)](https://codecov.io/gh/quantified-uncertainty/squiggle)
_An estimation language_.
## Get started
- [Gallery](https://www.squiggle-language.com/docs/Discussions/Gallery)
- [Squiggle playground](https://squiggle-language.com/playground)
- [Language basics](https://www.squiggle-language.com/docs/Guides/Language)
- [Squiggle functions source of truth](https://www.squiggle-language.com/docs/Guides/Functions)
- [Known bugs](https://www.squiggle-language.com/docs/Discussions/Bugs)
- [Original lesswrong sequence](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3)
- [Author your squiggle models as Observable notebooks](https://observablehq.com/@hazelfire/squiggle)
- [Use squiggle in VS Code](https://marketplace.visualstudio.com/items?itemName=QURI.vscode-squiggle)
## Our deployments ## Our deployments
- **website/docs prod**: https://squiggle-language.com - **website/docs prod**: https://squiggle-language.com
- **website/docs staging**: https://preview.squiggle-language.com - **website/docs staging**: https://staging--squiggle-documentation.netlify.app/
- **components storybook prod**: https://components.squiggle-language.com - **old playground**: https://playground.squiggle-language.com
- **components storybook staging**: https://preview-components.squiggle-language.com
- **legacy (2020) playground**: https://playground.squiggle-language.com
## Packages ## Packages
This monorepo has several packages that can be used for various purposes. All This monorepo has several packages that can be used for various purposes. All
the packages can be found in `packages`. the packages can be found in `packages`.
- `@quri/squiggle-lang` in `packages/squiggle-lang` contains the core language, particularly - `@quri/squiggle-lang` in `packages/squiggle-lang` contains the core language, particularly
an interface to parse squiggle expressions and return descriptions of distributions an interface to parse squiggle expressions and return descriptions of distributions
or results. or results.
- `@quri/squiggle-components` in `packages/components` contains React components that - `@quri/squiggle-components` in `packages/components` contains React components that
can be passed squiggle strings as props, and return a presentation of the result can be passed squiggle strings as props, and return a presentation of the result
of the calculation. of the calculation.
- `packages/website` is the main descriptive website for squiggle, - `@quri/squiggle-website` in `packages/website` The main descriptive website for squiggle,
it is hosted at `squiggle-language.com`. it is hosted at `squiggle-language.com`.
- `packages/vscode-ext` is the VS Code extension for writing estimation functions.
- `packages/cli` is an experimental way of using imports in squiggle, which is also on [npm](https://www.npmjs.com/package/squiggle-cli-experimental). The playground depends on the components library which then depends on the language. This means that if you wish to work on the components library, you will need to build (no need to bundle) the language, and as of this writing playground doesn't really work.
# Develop # Develop
For any project in the repo, begin by running `yarn` in the top level For any project in the repo, begin by running `yarn` in the top level (TODO: is this true?)
```sh ``` sh
yarn yarn
``` ```
Then use `turbo` to build the specific packages or the entire monorepo: See `packages/*/README.md` to work with whatever project you're interested in.
```sh ## `codium` for `rescript`
turbo run build
If you have `nix` installed with `flakes` enabled, you can build a `codium` in this repo for `rescript` development, if you don't want to pollute your machine's global editor with another mode/extension.
``` sh
nix develop
codium
``` ```
Or: The `nix develop` shell also provides `yarn`.
```sh
turbo run build --filter=@quri/squiggle-components
```
You can also run specific npm scripts for the package you're working on. See `packages/*/README.md` for the details.
# NixOS users
This repository requires the use of bundled binaries from node_modules, which
are not linked statically. The easiest way to get them working is to enable
[nix-ld](https://github.com/Mic92/nix-ld).
# Contributing # Contributing
See `CONTRIBUTING.md`. See `CONTRIBUTING.md`.

View File

@ -1,20 +0,0 @@
# The following code was provided by Nuño Sempere, it comes directly from the post https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3/p/j8o6sgRerE3tqNWdj
## Initial setup
yearly_probability_max = 0.95
yearly_probability_min = 0.66
period_probability_function(epsilon, yearly_probability) = 1 - (1 - yearly_probability) ^ (1 / epsilon)
probability_decayed(t, time_periods, period_probability) = 1 - (1 - period_probability) ^ (time_periods - t)
## Monthly decomposition
months_in_a_year=12
monthly_probability_min = period_probability_function(months_in_a_year, yearly_probability_min)
monthly_probability_max = period_probability_function(months_in_a_year, yearly_probability_max)
probability_decayed_monthly_min(t) = probability_decayed(t, months_in_a_year, monthly_probability_min)
probability_decayed_monthly_max(t) = probability_decayed(t, months_in_a_year, monthly_probability_max)
probability_decayed_monthly(t) = probability_decayed_monthly_min(t) to probability_decayed_monthly_max(t)
probability_decayed_monthly
## probability_decayed_monthly(6)
## mean(probability_decayed_monthly(6))

View File

@ -1,38 +0,0 @@
# This is a cost effectiveness analysis of givedirectly, originally done by givewell, and translated into Squiggle by Sam Nolan
donation_size = 10000
proportion_of_funding_available = beta(10, 2)
total_funding_available = donation_size * proportion_of_funding_available
household_size = 3.7 to 5.7
size_of_transfer = 800 to 1200
size_of_transfer_per_person = size_of_transfer / household_size
portion_invested = 0.3 to 0.5
amount_invested = portion_invested * size_of_transfer_per_person
amount_consumed = (1 - portion_invested) * size_of_transfer_per_person
return_on_investment = 0.08 to 0.12
increase_in_consumption_from_investments = return_on_investment * amount_invested
baseline_consumption = 200 to 350
log_increase_in_consumption = log(amount_consumed + baseline_consumption) + log(baseline_consumption)
log_increase_in_consumption_from_investment = log(increase_in_consumption_from_investments + baseline_consumption) + log(baseline_consumption)
investment_duration = 8 to 12
discount_rate = beta(1.004, 20)
present_value_excluding_last_year = log_increase_in_consumption_from_investment * (1 - (1 + discount_rate) ^ (-investment_duration)) / (log(1 + discount_rate))
percent_of_investment_returned = 0.15 to 0.25
pv_consumption_last_year = (log(baseline_consumption + amount_invested * (return_on_investment + percent_of_investment_returned)) - log(baseline_consumption)) / (1 + discount_rate)^investment_duration
total_pv_of_cash_transfer = pv_consumption_last_year + present_value_excluding_last_year + log_increase_in_consumption
discount_negative_spoiler = 0.03 to 0.07
value_discounting_spoiler = discount_negative_spoiler * total_pv_of_cash_transfer
consumption_increase_per_household = value_discounting_spoiler * household_size
amount_of_transfers_made = total_funding_available / size_of_transfer
total_increase_in_ln_consumption = amount_of_transfers_made * consumption_increase_per_household
total_increase_in_ln_consumption

View File

@ -1,3 +0,0 @@
xY1 = 99
aBa3 = xY1 * 2 + 1
aBa3 * xY1 + aBa3

9
flake-compat.nix Normal file
View File

@ -0,0 +1,9 @@
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
inherit (lock.nodes.flake-compat.locked) owner repo rev narHash;
flake-compat = builtins.fetchTarball {
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
sha256 = narHash;
};
in
import flake-compat { src = ./.; }

View File

@ -1,75 +1,40 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "flake-compat": {
"flake": false,
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1641205782,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=",
"owner": "numtide", "owner": "edolstra",
"repo": "flake-utils", "repo": "flake-compat",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "edolstra",
"repo": "flake-utils", "repo": "flake-compat",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gentype": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1661855866,
"narHash": "sha256-+q0OOTyaq8eOn9BOWdPOCtSDOISW4A59v3mq3JOZyug=",
"owner": "rescript-association",
"repo": "genType",
"rev": "6b5f164b4f6ced456019b7579a0ab7e0a86518ad",
"type": "github"
},
"original": {
"owner": "rescript-association",
"repo": "genType",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1661617163, "lastModified": 1647893727,
"narHash": "sha256-NN9Ky47j8ohgPhA9JZyfkYIbbAo6RJkGz+7h8/exVpE=", "narHash": "sha256-pOi7VdCb+s5Cwh5CS7YEZVRgH9uCmE87J5W7iXv29Ck=",
"owner": "NixOS", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0ba2543f8c855d7be8e90ef6c8dc89c1617e8a08", "rev": "1ec61dd4167f04be8d05c45780818826132eea0d",
"type": "github" "type": "github"
}, },
"original": { "original": {
"id": "nixpkgs", "owner": "nixos",
"ref": "nixos-22.05", "ref": "nixos-unstable",
"type": "indirect" "repo": "nixpkgs",
"type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-compat": "flake-compat",
"gentype": "gentype",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

173
flake.nix
View File

@ -1,99 +1,94 @@
{ {
description = "Squiggle packages"; description = "Building codium for rescript development";
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixos-22.05"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
gentype = { flake-compat = {
url = "github:rescript-association/genType"; url = "github:edolstra/flake-compat";
inputs.nixpkgs.follows = "nixpkgs"; flake = false;
}; };
flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, gentype, flake-utils }: outputs =
{ self
, nixpkgs
, flake-compat
}:
let let
# Generate a user-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate; version = builtins.substring 0 8 self.lastModifiedDate;
overlays = [ # System types to support.
(final: prev: { supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
# set the node version here # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'.
nodejs = prev.nodejs-18_x; forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
# The override is the only way to get it into mkYarnModules # Nixpkgs instantiated for supported system types.
}) nixpkgsFor = forAllSystems (system:
]; import nixpkgs {
commonFn = pkgs: {
buildInputs = with pkgs; [ nodejs yarn ];
prettier = with pkgs.nodePackages; [ prettier ];
which = [ pkgs.which ];
};
gentypeOutputFn = pkgs: gentype.outputs.packages.${pkgs.system}.default;
langFn = { pkgs, ... }:
# Probably doesn't work on i686-linux
import ./nix/squiggle-lang.nix {
inherit pkgs commonFn gentypeOutputFn;
};
componentsFn = { pkgs, ... }:
import ./nix/squiggle-components.nix { inherit pkgs commonFn langFn; };
websiteFn = { pkgs, ... }:
import ./nix/squiggle-website.nix {
inherit pkgs commonFn langFn componentsFn;
};
vscodeextFn = { pkgs, ... }:
import ./nix/squiggle-vscode.nix {
inherit pkgs commonFn langFn componentsFn;
};
cliFn = { pkgs, ... }:
import ./nix/squiggle-cli.nix {
inherit pkgs commonFn;
};
# local machines
localFlakeOutputs = { pkgs, ... }:
let
lang = langFn pkgs;
components = componentsFn pkgs;
website = websiteFn pkgs;
vscodeext = vscodeextFn pkgs;
cli = cliFn pkgs;
in {
# validating
checks = flake-utils.lib.flattenTree {
lang-lint = lang.lint;
lang-test = lang.test;
components-lint = components.lint;
docusaurus-lint = website.lint;
cli-lint = cli.lint;
};
# building
packages = flake-utils.lib.flattenTree {
default = components.build;
lang = lang.build;
lang-bundle = lang.bundle;
lang-test = lang.test;
components = components.build;
components-bundle = components.bundle;
# Lint
lang-lint = lang.lint;
components-lint = components.lint;
docusaurus-lint = website.lint;
vscode-lint = vscodeext.lint;
cli-lint = cli.lint;
};
# developing
devShells = let shellNix = import ./nix/shell.nix { inherit pkgs; };
in flake-utils.lib.flattenTree {
default = shellNix.all;
js = shellNix.just-js;
};
};
in flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system; inherit system;
overlays = overlays; overlays = [ self.overlay ];
}; });
in
in localFlakeOutputs pkgs); {
overlay = final: prev: { };
# the default devShell used when running `nix develop`
devShell = forAllSystems (system: self.devShells.${system}.defaultShell);
devShells = forAllSystems (system:
let
pkgs = nixpkgsFor."${system}";
in
{
# In case we don't want to provide an editor, this defaultShell will provide only coq packages we need.
defaultShell = pkgs.mkShell {
buildInputs = with pkgs; [
yarn
yarn2nix
nodePackages.npm
nodejs
patchelf
(pkgs.vscode-with-extensions.override {
vscode = pkgs.vscodium;
vscodeExtensions = pkgs.vscode-utils.extensionsFromVscodeMarketplace [
{
name = "rescript-vscode";
publisher = "chenglou92";
version = "1.2.1";
sha256 = "sha256-7/YakKtJ4WhgAR4rZltrq8g4TtM5QZ2spbrEUrNoXVg=";
}
{
name = "vim";
publisher = "vscodevim";
version = "1.22.2";
sha256 = "sha256-dtIlgODzRdoMKnG9050ZcCX3w15A/R3FaMc+ZylvBbU=";
}
{
name = "vscode-typescript-next";
publisher = "ms-vscode";
version = "4.7.20220323";
sha256 = "sha256-mjiBCyg5As/XAU9I5k6jEZWGJA3P6P5o1roe2bS7aUI=";
}
{
name = "nix-ide";
publisher = "jnoortheen";
version = "0.1.20";
sha256 = "sha256-Q6X41I68m0jaCXaQGEFOoAbSUrr/wFhfCH5KrduOtZo=";
}
{
name = "json";
publisher = "ZainChen";
version = "2.0.2";
sha256 = "sha256-nC3Q8KuCtn/jg1j/NaAxWGvnKe/ykrPm2PUjfsJz8aI=";
}
{
name = "prettier-vscode";
publisher = "esbenp";
version = "9.3.0";
sha256 = "sha256-hJgPjWf7a8+ltjmXTK8U/MwqgIZqBjmcCfHsAk2G3PA=";
}
];
})
];
};
}
);
};
} }

View File

@ -1 +0,0 @@
Visit `quantified-uncertainty.cachix.org` for information about how to add our binary cache to your local dev environment.

View File

@ -1,25 +0,0 @@
{ pkgs }:
with pkgs;
let
js = [ yarn nodejs nodePackages.ts-node ];
rust = [
wasm-pack
cargo
rustup
pkg-config
libressl
rustfmt
wasmtime
binaryen
wasm-bindgen-cli
];
in {
all = mkShell {
name = "squiggle_yarn-wasm-devshell";
buildInputs = builtins.concatLists [ js rust [ nixfmt ] ];
};
just-js = mkShell {
name = "squiggle_yarn-devshell";
buildInputs = js ++ [ nixfmt ];
};
}

View File

@ -1,13 +0,0 @@
{ pkgs, commonFn }:
rec {
common = commonFn pkgs;
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-cli-lint";
buildInputs = common.buildInputs ++ common.prettier;
src = ../packages/cli;
buildPhase = "prettier --check .";
installPhase = "mkdir -p $out";
};
}

View File

@ -1,75 +0,0 @@
{ pkgs, commonFn, langFn }:
rec {
common = commonFn pkgs;
lang = langFn pkgs;
componentsPackageJson = let
raw = pkgs.lib.importJSON ../packages/components/package.json;
modified =
pkgs.lib.recursiveUpdate raw { dependencies.react-dom = "^18.2.0"; };
packageJsonString = builtins.toJSON modified;
in pkgs.writeText "packages/components/patched-package.json"
packageJsonString;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-components_yarnsource";
buildInputs = common.buildInputs;
src = ../packages/components;
packageJSON = componentsPackageJson;
yarnLock = ../yarn.lock;
packageResolutions."@quri/squiggle-lang" = lang.build;
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-components-lint";
src = ../packages/components;
buildInputs = common.buildInputs ++ common.prettier;
buildPhase = "yarn lint";
installPhase = "mkdir -p $out";
};
build = pkgs.stdenv.mkDerivation {
name = "squiggle-components-build";
src = yarn-source + "/libexec/@quri/squiggle-components";
buildInputs = common.buildInputs;
buildPhase = ''
cp -r node_modules/@quri/squiggle-lang deps/@quri
pushd deps/@quri/squiggle-components
yarn --offline build:cjs
yarn --offline build:css
popd
'';
installPhase = ''
mkdir -p $out
# annoying hack because permissions on transitive dependencies later on
mv deps/@quri/squiggle-components/node_modules deps/@quri/squiggle-components/NODE_MODULES
mv node_modules deps/@quri/squiggle-components
# patching .gitignore so flake keeps build artefacts
sed -i /dist/d deps/@quri/squiggle-components/.gitignore
cp -r deps/@quri/squiggle-components/. $out
'';
};
bundle = pkgs.stdenv.mkDerivation {
name = "squiggle-components-bundle";
src = yarn-source + "/libexec/@quri/squiggle-components";
buildInputs = common.buildInputs;
buildPhase = ''
cp -r node_modules/@quri/squiggle-lang deps/@quri
pushd deps/@quri/squiggle-components
yarn --offline bundle
popd
'';
installPhase = ''
mkdir -p $out
# annoying hack because permissions on transitive dependencies later on
mv deps/@quri/squiggle-components/node_modules deps/@quri/squiggle-components/NODE_MODULES
mv node_modules deps/@quri/squiggle-components
# patching .gitignore so flake keeps build artefacts
sed -i /dist/d deps/@quri/squiggle-components/.gitignore
cp -r deps/@quri/squiggle-components/. $out
'';
};
}

View File

@ -1,116 +0,0 @@
{ pkgs, commonFn, gentypeOutputFn }:
rec {
common = commonFn pkgs;
langPackageJson = let
raw = pkgs.lib.importJSON ../packages/squiggle-lang/package.json;
modified = pkgs.lib.recursiveUpdate raw {
devDependencies."@types/lodash" = "^4.14.167";
};
packageJsonString = builtins.toJSON modified;
in pkgs.writeText "packages/squiggle-lang/patched-package.json"
packageJsonString;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-lang_yarnsource";
src = ../packages/squiggle-lang;
packageJSON = langPackageJson;
yarnLock = ../yarn.lock;
pkgConfig = {
rescript = {
buildInputs = common.which
++ (if pkgs.system != "i686-linux" then [ pkgs.gcc_multi ] else [ ]);
postInstall = ''
echo "PATCHELF'ING RESCRIPT EXECUTABLES (INCL NINJA)"
# Patching interpreter for linux/*.exe's
THE_LD=$(patchelf --print-interpreter $(which mkdir))
patchelf --set-interpreter $THE_LD linux/*.exe && echo "- patched interpreter for linux/*.exe's"
# Replacing needed shared library for linux/ninja.exe
THE_SO=$(find /nix/store/*/lib64 -name libstdc++.so.6 | head -n 1)
patchelf --replace-needed libstdc++.so.6 $THE_SO linux/ninja.exe && echo "- replaced needed for linux/ninja.exe"
'';
};
gentype = {
postInstall = ''
mv gentype.exe ELFLESS-gentype.exe
cp ${gentypeOutputFn pkgs}/src/GenType.exe gentype.exe
'';
};
};
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-lint";
src = yarn-source + "/libexec/@quri/squiggle-lang/deps/@quri/squiggle-lang";
buildInputs = common.buildInputs ++ common.prettier;
buildPhase = ''
yarn lint:prettier
yarn lint:rescript
'';
installPhase = "mkdir -p $out";
};
build = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-build";
# `peggy` is in the `node_modules` that's adjacent to `deps`.
src = yarn-source + "/libexec/@quri/squiggle-lang";
buildInputs = common.buildInputs;
buildPhase = ''
# so that the path to ppx doesn't need to be patched.
mv node_modules deps
pushd deps/@quri/squiggle-lang
yarn --offline build:peggy
yarn --offline build:rescript
yarn --offline build:typescript
# custom gitignore so that the flake keeps build artefacts
mv .gitignore GITIGNORE
sed -i /Reducer_Peggy_GeneratedParser.js/d GITIGNORE
sed -i /ReducerProject_IncludeParser.js/d GITIGNORE
sed -i /\*.bs.js/d GITIGNORE
sed -i /\*.gen.ts/d GITIGNORE
sed -i /\*.gen.tsx/d GITIGNORE
sed -i /\*.gen.js/d GITIGNORE
sed -i /helpers.js/d GITIGNORE
popd
'';
installPhase = ''
mkdir -p $out
# mkdir -p $out/node_modules
mv deps/@quri/squiggle-lang/GITIGNORE deps/@quri/squiggle-lang/.gitignore
# annoying hack because permissions on transitive dependencies later on
mv deps/@quri/squiggle-lang/node_modules deps/@quri/squiggle-lang/NODE_MODULES
mv deps/node_modules deps/@quri/squiggle-lang
# the proper install phase
cp -r deps/@quri/squiggle-lang/. $out
'';
};
test = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-test";
src = build;
buildInputs = common.buildInputs;
buildPhase = ''
yarn --offline test
'';
installPhase = ''
mkdir -p $out
cp -r . $out
'';
};
bundle = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-bundle";
src = test;
buildInputs = common.buildInputs;
buildPhase = ''
yarn --offline bundle
'';
installPhase = ''
mkdir -p $out
cp -r dist $out
cp *.json $out/dist
'';
};
}

View File

@ -1,24 +0,0 @@
{ pkgs, commonFn, langFn, componentsFn }:
rec {
common = commonFn pkgs;
lang = langFn pkgs;
components = componentsFn pkgs;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-vscodeext_yarnsource";
src = ../packages/vscode-ext;
packageJson = ../packages/vscode-ext/package.json;
yarnLock = ../yarn.lock;
packageResolutions."@quri/squiggle-lang" = lang.build;
packageResolutions."@quri/squiggle-components" = components.build;
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-vscode-lint";
buildInputs = common.buildInputs ++ common.prettier;
src =
../packages/vscode-ext; # yarn-source + "/libexec/vscode-squiggle/deps/vscode-squiggle";
buildPhase = "prettier --check .";
installPhase = "mkdir -p $out";
};
}

View File

@ -1,30 +0,0 @@
{ pkgs, commonFn, langFn, componentsFn }:
rec {
common = commonFn pkgs;
lang = langFn pkgs;
components = componentsFn pkgs;
websitePackageJson = let
raw = pkgs.lib.importJSON ../packages/website/package.json;
modified = pkgs.lib.recursiveUpdate raw {
dependencies.postcss-import = "^14.1.0";
dependencies.tailwindcss = "^3.1.8";
};
packageJsonString = builtins.toJSON modified;
in pkgs.writeText "packages/website/patched-package.json" packageJsonString;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-website_yarnsource";
src = ../packages/website;
packageJSON = websitePackageJson;
yarnLock = ../yarn.lock;
packageResolutions."@quri/squiggle-lang" = lang.build;
packageResolutions."@quri/squiggle-components" = components.build;
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-website-lint";
buildInputs = common.buildInputs ++ common.prettier;
src = ../packages/website;
buildPhase = "yarn lint";
installPhase = "mkdir -p $out";
};
}

View File

@ -4,16 +4,11 @@
"scripts": { "scripts": {
"nodeclean": "rm -r node_modules && rm -r packages/*/node_modules" "nodeclean": "rm -r node_modules && rm -r packages/*/node_modules"
}, },
"devDependencies": {
"prettier": "^2.7.1",
"turbo": "^1.5.5"
},
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"resolutions": { "resolutions": {
"@types/react": "^18.0.1", "@types/react": "^17.0.43"
"react": "^18.0.0"
}, },
"packageManager": "yarn@1.22.17" "packageManager": "yarn@1.22.17"
} }

View File

@ -1,4 +0,0 @@
## Artifacts
*.swp
/node_modules/
yarn-error.log

View File

@ -1,49 +0,0 @@
## Squiggle CLI
This package can be used to incorporate a very simple `import` system into Squiggle.
To use, write special files with a `.squiggleU` file type. In these files, you can write lines like,
```
@import(models/gdp_over_time.squiggle, gdpOverTime)
gdpOverTime(2.5)
```
The imports will be replaced with the contents of the file in `models/gdp_over_time.squiggle` upon compilation. The `.squiggleU` file will be converted into a `.squiggle` file with the `import` statement having this replacement.
## Running
### `npx squiggle-cli-experimental compile`
Runs compilation in the current directory and all of its subdirectories.
### `npx squiggle-cli-experimental watch`
Watches `.squiggleU` files in the current directory (and subdirectories) and rebuilds them when they are saved. Note that this will _not_ rebuild files when their dependencies are changed, just when they are changed directly.
## Further instructions
The above requires having node, npm and npx. To install the first two, see [here](https://nodejs.org/en/), to install npx, run:
```
npm install -g npx
```
Alternatively, you can run the following without the need for npx:
```
npm install squiggle-cli-experimental
node node_modules/squiggle-cli-experimental/index.js compile
```
or you can add a script to your `package.json`, like:
```
...
scripts: {
"compile": "squiggle-cli-experimental compile"
}
...
```
This can be run with `npm run compile`. `npm` knows how to reach into the node_modules directly, so it's not necessary to specify that.

View File

@ -1,96 +0,0 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import indentString from "indent-string";
import chokidar from "chokidar";
import chalk from "chalk";
import { Command } from "commander";
import glob from "glob";
const processFile = (fileName, seen = []) => {
const normalizedFileName = path.resolve(fileName);
if (seen.includes(normalizedFileName)) {
throw new Error(`Recursive dependency for file ${fileName}`);
}
const fileContents = fs.readFileSync(fileName, "utf-8");
if (!fileName.endsWith(".squiggleU")) {
return fileContents;
}
const regex = /\@import\(\s*([^)]+?)\s*\)/g;
const matches = Array.from(fileContents.matchAll(regex)).map((r) =>
r[1].split(/\s*,\s*/)
);
const newContent = fileContents.replaceAll(regex, "");
const appendings = [];
matches.forEach((r) => {
const importFileName = r[0];
const rename = r[1];
const item = fs.statSync(importFileName);
if (item.isFile()) {
const data = processFile(importFileName, [...seen, normalizedFileName]);
if (data) {
const importString = `${rename} = {\n${indentString(data, 2)}\n}\n`;
appendings.push(importString);
}
} else {
console.log(
chalk.red(`Import Error`) +
`: ` +
chalk.cyan(importFileName) +
` not found in file ` +
chalk.cyan(fileName) +
`. Make sure the @import file names all exist in this repo.`
);
}
});
const imports = appendings.join("\n");
const newerContent = imports.concat(newContent);
return newerContent;
};
const run = (fileName) => {
const content = processFile(fileName);
const parsedPath = path.parse(path.resolve(fileName));
const newFilename = `${parsedPath.dir}/${parsedPath.name}.squiggle`;
fs.writeFileSync(newFilename, content);
console.log(chalk.cyan(`Updated ${fileName} -> ${newFilename}`));
};
const compile = () => {
glob("**/*.squiggleU", (_err, files) => {
files.forEach(run);
});
};
const watch = () => {
chokidar
.watch("**.squiggleU")
.on("ready", () => console.log(chalk.green("Ready!")))
.on("change", (event, _) => {
run(event);
});
};
const program = new Command();
program
.name("squiggle-utils")
.description("CLI to transform squiggle files with @imports")
.version("0.0.1");
program
.command("watch")
.description("watch files and compile on the fly")
.action(watch);
program
.command("compile")
.description("compile all .squiggleU files into .squiggle files")
.action(compile);
program.parse();

View File

@ -1,23 +0,0 @@
{
"name": "squiggle-cli-experimental",
"version": "0.0.3",
"main": "index.js",
"homepage": "https://squiggle-language.com",
"author": "Quantified Uncertainty Research Institute",
"bin": "index.js",
"type": "module",
"scripts": {
"start": "node .",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"license": "MIT",
"dependencies": {
"chalk": "^5.1.0",
"chokidar": "^3.5.3",
"commander": "^9.4.1",
"fs": "^0.0.1-security",
"glob": "^8.0.3",
"indent-string": "^5.0.0"
}
}

View File

@ -1,4 +1,5 @@
dist/ dist
build
node_modules
storybook-static storybook-static
src/styles/base.css .storybook
src/styles/forms.css

View File

@ -1,37 +1,31 @@
//const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); //const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const custom = require("../webpack.config.js"); const custom = require('../webpack.config.js');
module.exports = { module.exports = {
webpackFinal: async (config) => { webpackFinal: async (config) => {
config.resolve.alias = custom.resolve.alias; config.resolve.alias = custom.resolve.alias;
return { return { ...config, module: { ...config.module, rules: config.module.rules.concat(custom.module.rules.filter(x => x.loader === "ts-loader")) } };
...config,
module: {
...config.module,
rules: config.module.rules.concat(
custom.module.rules.filter((x) => x.loader === "ts-loader")
),
},
};
}, },
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], "stories": [
addons: [ "../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/preset-create-react-app", "@storybook/preset-create-react-app"
], ],
framework: "@storybook/react", "framework": "@storybook/react",
core: { "core": {
builder: "webpack5", "builder": "webpack5"
}, },
typescript: { typescript: {
check: false, check: false,
checkOptions: {}, checkOptions: {},
reactDocgen: "react-docgen-typescript", reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: { reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true, shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
}, },
}, },
}; }

View File

@ -1,15 +1,3 @@
import "../src/styles/main.css";
import "!style-loader!css-loader!postcss-loader!../src/styles/main.css";
import { SquiggleContainer } from "../src/components/SquiggleContainer";
export const decorators = [
(Story) => (
<SquiggleContainer>
<Story />
</SquiggleContainer>
),
];
export const parameters = { export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" }, actions: { argTypesRegex: "^on[A-Z].*" },
controls: { controls: {
@ -18,4 +6,4 @@ export const parameters = {
date: /Date$/, date: /Date$/,
}, },
}, },
}; }

View File

@ -1,67 +1,8 @@
[![npm version](https://badge.fury.io/js/@quri%2Fsquiggle-components.svg)](https://www.npmjs.com/package/@quri/squiggle-components) # Squiggle Components
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/quantified-uncertainty/squiggle/blob/develop/LICENSE)
# Squiggle components This package contains all the components for squiggle. These can be used either as a library or hosted as a [storybook](https://storybook.js.org/).
This package contains the react components for squiggle. These can be used either as a library or hosted as a [storybook](https://storybook.js.org/). # Build for development
The `@quri/squiggle-components` package offers several components and utilities for people who want to embed Squiggle components into websites.
# Usage in a `react` project
For example, in a fresh `create-react-app` project
```sh
yarn add @quri/squiggle-components
```
Add to `App.js`:
```jsx
import { SquiggleEditor } from "@quri/squiggle-components";
<SquiggleEditor
defaultCode="x = beta($alpha, 10); x + $shift"
jsImports={{ alpha: 3, shift: 20 }}
/>;
```
# Usage in a Nextjs project
For now, `squiggle-components` requires the `window` property, so using the package in nextjs requires dynamic loading:
```
import React from "react";
import { SquiggleChart } from "@quri/squiggle-components";
import dynamic from "next/dynamic";
const SquiggleChart = dynamic(
() => import("@quri/squiggle-components").then((mod) => mod.SquiggleChart),
{
loading: () => <p>Loading...</p>,
ssr: false,
}
);
export function DynamicSquiggleChart({ squiggleString }) {
if (squiggleString == "") {
return null;
} else {
return (
<SquiggleChart
defaultCode={squiggleString}
width={445}
height={200}
showSummary={true}
/>
);
}
}
```
# Build storybook for development
We assume that you had run `yarn` at monorepo level, installing dependencies. We assume that you had run `yarn` at monorepo level, installing dependencies.
@ -79,3 +20,10 @@ Run a development server
```sh ```sh
yarn start yarn start
``` ```
And build artefacts for production,
```sh
yarn bundle # builds components library
yarn build # builds storybook app
```

View File

@ -1,6 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
setupFilesAfterEnv: ["<rootDir>/test/setup.js"],
testEnvironment: "jsdom",
};

View File

@ -1,92 +1,38 @@
{ {
"name": "@quri/squiggle-components", "name": "@quri/squiggle-components",
"version": "0.5.0", "version": "0.1.8",
"license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/react-dom": "^1.0.0", "@quri/squiggle-lang": "0.2.2",
"@floating-ui/react-dom-interactions": "^0.10.1", "@testing-library/jest-dom": "^5.16.4",
"@headlessui/react": "^1.7.3", "@testing-library/react": "^13.0.0",
"@heroicons/react": "^1.0.6", "@testing-library/user-event": "^14.0.4",
"@hookform/resolvers": "^2.9.8", "@types/jest": "^27.4.0",
"@quri/squiggle-lang": "^0.5.0", "@types/lodash": "^4.14.181",
"@react-hook/size": "^2.1.2", "@types/node": "^17.0.23",
"@types/uuid": "^8.3.4", "@types/react": "^18.0.1",
"clsx": "^1.2.1", "@types/react-dom": "^18.0.0",
"framer-motion": "^7.5.3", "antd": "^4.19.3",
"lodash": "^4.17.21",
"react": "^18.1.0",
"react-ace": "^10.1.0",
"react-hook-form": "^7.37.0",
"react-use": "^17.4.0",
"react-vega": "^7.6.0",
"uuid": "^9.0.0",
"vega": "^5.22.1",
"vega-embed": "^6.21.0",
"vega-lite": "^5.5.0",
"vscode-uri": "^3.0.6",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.18.6",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.5.12",
"@storybook/addon-links": "^6.5.12",
"@storybook/builder-webpack5": "^6.5.12",
"@storybook/manager-webpack5": "^6.5.12",
"@storybook/node-logger": "^6.5.9",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.12",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^27.5.0",
"@types/lodash": "^4.14.186",
"@types/node": "^18.8.3",
"@types/react": "^18.0.21",
"@types/styled-components": "^5.1.26",
"@types/uuid": "^8.3.4",
"@types/webpack": "^5.28.0",
"canvas": "^2.10.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jest": "^29.0.3", "lodash": "^4.17.21",
"jest-environment-jsdom": "^29.1.2", "react": "^18.0.0",
"jsdom": "^20.0.1", "react-dom": "^18.0.0",
"mini-css-extract-plugin": "^2.6.1", "react-scripts": "5.0.0",
"postcss-cli": "^10.0.0", "react-vega": "^7.5.0",
"postcss-import": "^15.0.0", "react-ace": "9.5.0",
"postcss-loader": "^7.0.1", "styled-components": "^5.3.5",
"postcss-nesting": "^10.2.0", "tsconfig-paths-webpack-plugin": "^3.5.2",
"react": "^18.1.0", "typescript": "^4.6.3",
"react-scripts": "^5.0.1", "vega": "^5.22.1",
"style-loader": "^3.3.1", "vega-embed": "^6.20.6",
"tailwindcss": "^3.1.8", "vega-lite": "^5.2.0",
"ts-jest": "^29.0.3", "web-vitals": "^2.1.4",
"ts-loader": "^9.4.1", "webpack-cli": "^4.9.2"
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.4",
"web-vitals": "^3.0.3",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}, },
"scripts": { "scripts": {
"start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public", "start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
"build:cjs": "rm -rf dist/src && rm -f dist/tsconfig.tsbuildinfo && tsc -b", "build": "tsc -b && build-storybook -s public",
"build:css": "postcss ./src/styles/main.css -o ./dist/main.css",
"build:storybook": "build-storybook -s public",
"build": "yarn run build:cjs && yarn run build:css && yarn run build:storybook",
"bundle": "webpack", "bundle": "webpack",
"all": "yarn bundle && yarn build", "all": "yarn bundle && yarn build"
"lint": "prettier --check .",
"format": "prettier --write .",
"prepack": "yarn run build:cjs && yarn run bundle",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"test:profile": "node --cpu-prof node_modules/.bin/jest --runInBand"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -116,10 +62,31 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"devDependencies": {
"@types/styled-components": "^5.1.24",
"css-loader": "^6.7.1",
"style-loader": "^3.3.1",
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
"@storybook/addon-actions": "^6.4.20",
"@storybook/addon-essentials": "^6.4.20",
"@storybook/addon-links": "^6.4.20",
"@storybook/builder-webpack5": "^6.4.20",
"@storybook/manager-webpack5": "^6.4.20",
"@storybook/node-logger": "^6.4.20",
"@storybook/preset-create-react-app": "^4.1.0",
"@storybook/react": "^6.4.20",
"@types/webpack": "^4.41.32",
"prettier": "^2.6.2",
"react-codejar": "^1.1.2",
"ts-loader": "^9.2.8",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1"
},
"resolutions": { "resolutions": {
"@types/react": "17.0.43" "@types/react": "17.0.43"
}, },
"source": "./src/index.ts", "source": "./src/index.ts",
"main": "./dist/src/index.js", "main": "dist/bundle.js",
"types": "./dist/src/index.d.ts" "types": "dist/src/index.d.ts"
} }

View File

@ -1,9 +0,0 @@
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
cssnano: {},
},
};

File diff suppressed because one or more lines are too long

View File

@ -1,86 +0,0 @@
import * as React from "react";
import {
XCircleIcon,
InformationCircleIcon,
CheckCircleIcon,
} from "@heroicons/react/solid";
import clsx from "clsx";
export const Alert: React.FC<{
heading: string;
backgroundColor: string;
headingColor: string;
bodyColor: string;
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
iconColor: string;
children?: React.ReactNode;
}> = ({
heading = "Error",
backgroundColor,
headingColor,
bodyColor,
icon: Icon,
iconColor,
children,
}) => {
return (
<div className={clsx("rounded-md p-4", backgroundColor)} role="status">
<div className="flex">
<Icon
className={clsx("h-5 w-5 flex-shrink-0", iconColor)}
aria-hidden="true"
/>
<div className="ml-3 grow">
<header className={clsx("text-sm font-medium", headingColor)}>
{heading}
</header>
{children && React.Children.count(children) ? (
<div className={clsx("mt-2 text-sm", bodyColor)}>{children}</div>
) : null}
</div>
</div>
</div>
);
};
export const ErrorAlert: React.FC<{
heading: string;
children?: React.ReactNode;
}> = (props) => (
<Alert
{...props}
backgroundColor="bg-red-100"
headingColor="text-red-800"
bodyColor="text-red-700"
icon={XCircleIcon}
iconColor="text-red-400"
/>
);
export const MessageAlert: React.FC<{
heading: string;
children?: React.ReactNode;
}> = (props) => (
<Alert
{...props}
backgroundColor="bg-slate-100"
headingColor="text-slate-700"
bodyColor="text-slate-700"
icon={InformationCircleIcon}
iconColor="text-slate-400"
/>
);
export const SuccessAlert: React.FC<{
heading: string;
children?: React.ReactNode;
}> = (props) => (
<Alert
{...props}
backgroundColor="bg-green-50"
headingColor="text-green-800"
bodyColor="text-green-700"
icon={CheckCircleIcon}
iconColor="text-green-400"
/>
);

View File

@ -1,53 +1,33 @@
import _ from "lodash"; import _ from "lodash";
import React, { FC, useMemo, useRef } from "react"; import React, { FC } from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang"; import "ace-builds/src-noconflict/mode-golang";
import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-github";
import { SqLocation } from "@quri/squiggle-lang";
interface CodeEditorProps { interface CodeEditorProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onSubmit?: () => void;
oneLine?: boolean; oneLine?: boolean;
width?: number; width?: number;
height: number;
showGutter?: boolean;
errorLocations?: SqLocation[];
} }
export const CodeEditor: FC<CodeEditorProps> = ({ export let CodeEditor: FC<CodeEditorProps> = ({
value, value,
onChange, onChange,
onSubmit,
height,
oneLine = false, oneLine = false,
showGutter = false, }: CodeEditorProps) => {
errorLocations = [], let lineCount = value.split("\n").length;
}) => { let id = _.uniqueId();
const lineCount = value.split("\n").length;
const id = useMemo(() => _.uniqueId(), []);
// this is necessary because AceEditor binds commands on mount, see https://github.com/securingsincity/react-ace/issues/684
const onSubmitRef = useRef<typeof onSubmit | null>(null);
onSubmitRef.current = onSubmit;
const editorEl = useRef<AceEditor | null>(null);
return ( return (
<AceEditor <AceEditor
ref={editorEl}
value={value} value={value}
mode="golang" mode="golang"
theme="github" theme="github"
width="100%" width={"100%"}
fontSize={14} minLines={oneLine ? lineCount : 15}
height={String(height) + "px"} maxLines={oneLine ? lineCount : 15}
minLines={oneLine ? lineCount : undefined} showGutter={false}
maxLines={oneLine ? lineCount : undefined}
showGutter={showGutter}
highlightActiveLine={false} highlightActiveLine={false}
showPrintMargin={false} showPrintMargin={false}
onChange={onChange} onChange={onChange}
@ -55,22 +35,11 @@ export const CodeEditor: FC<CodeEditorProps> = ({
editorProps={{ editorProps={{
$blockScrolling: true, $blockScrolling: true,
}} }}
setOptions={{}} setOptions={{
commands={[ enableBasicAutocompletion: false,
{ enableLiveAutocompletion: false,
name: "submit", }}
bindKey: { mac: "Cmd-Enter", win: "Ctrl-Enter" },
exec: () => onSubmitRef.current?.(),
},
]}
markers={errorLocations?.map((location) => ({
startRow: location.start.line - 1,
startCol: location.start.column - 1,
endRow: location.end.line - 1,
endCol: location.end.column - 1,
className: "ace-error-marker",
type: "text",
}))}
/> />
); );
}; };
export default CodeEditor;

View File

@ -0,0 +1,124 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import type {
DistPlus,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as chartSpecification from "../vega-specs/spec-distributions.json";
let SquiggleVegaChart = createClassFromSpec({
spec: chartSpecification as Spec,
});
export const DistPlusChart: React.FC<{
distPlus: DistPlus;
width: number;
height: number;
}> = ({ distPlus, width, height }) => {
let shape = distPlus.pointSetDist;
if (shape.tag === "Continuous") {
let xyShape = shape.value.xyShape;
let totalY = xyShape.ys.reduce((a, b) => a + b);
let total = 0;
let cdf = xyShape.ys.map((y) => {
total += y;
return total / totalY;
});
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({
cdf: (c * 100).toFixed(2) + "%",
x: x,
y: y,
}));
return (
<SquiggleVegaChart
width={width}
height={height}
data={{ con: values }}
actions={false}
/>
);
} else if (shape.tag === "Discrete") {
let xyShape = shape.value.xyShape;
let totalY = xyShape.ys.reduce((a, b) => a + b);
let total = 0;
let cdf = xyShape.ys.map((y) => {
total += y;
return total / totalY;
});
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({
cdf: (c * 100).toFixed(2) + "%",
x: x,
y: y,
}));
return <SquiggleVegaChart data={{ dis: values }} actions={false} />;
} else if (shape.tag === "Mixed") {
let discreteShape = shape.value.discrete.xyShape;
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
let continuousShape = shape.value.continuous.xyShape;
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
interface labeledPoint {
x: number;
y: number;
type: "discrete" | "continuous";
}
let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({
x: x,
y: y,
type: "discrete",
}));
let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({
x: x,
y: y,
type: "continuous",
}));
let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x");
let totalContinuous = 1 - totalDiscrete;
let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b);
let total = 0;
let cdf = sortedPoints.map((point: labeledPoint) => {
if (point.type === "discrete") {
total += point.y;
return total;
} else if (point.type === "continuous") {
total += (point.y / totalY) * totalContinuous;
return total;
}
});
interface cdfLabeledPoint {
cdf: string;
x: number;
y: number;
type: "discrete" | "continuous";
}
let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith(
cdf,
sortedPoints,
(c: number, point: labeledPoint) => ({
...point,
cdf: (c * 100).toFixed(2) + "%",
})
);
let continuousValues = cdfLabeledPoint.filter(
(x) => x.type === "continuous"
);
let discreteValues = cdfLabeledPoint.filter((x) => x.type === "discrete");
return (
<SquiggleVegaChart
data={{ con: continuousValues, dis: discreteValues }}
actions={false}
/>
);
}
};

View File

@ -1,222 +0,0 @@
import * as React from "react";
import {
SqDistribution,
result,
SqDistributionError,
resultMap,
SqRecord,
environment,
SqDistributionTag,
} from "@quri/squiggle-lang";
import { Vega } from "react-vega";
import { ErrorAlert } from "./Alert";
import { useSize } from "react-use";
import {
buildVegaSpec,
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 = {
/** Whether to show a summary of means, stdev, percentiles etc */
showSummary: boolean;
actions?: boolean;
} & DistributionChartSpecOptions;
export type DistributionChartProps = {
plot: Plot;
environment: environment;
width?: number;
height: number;
xAxisType?: "number" | "dateTime";
} & DistributionPlottingSettings;
export function defaultPlot(distribution: SqDistribution): Plot {
return { distributions: [{ name: "default", distribution }] };
}
export function makePlot(record: SqRecord): Plot | void {
const plotResult = parsePlot(record);
if (plotResult.tag === "Ok") {
return plotResult.value;
}
}
export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const {
plot,
environment,
height,
showSummary,
width,
logX,
actions = false,
} = props;
const [sized] = useSize((size) => {
const shapes = flattenResult(
plot.distributions.map((x) =>
resultMap(x.distribution.pointSet(environment), (pointSet) => ({
name: x.name,
// color: x.color, // not supported yet
...pointSet.asShape(),
}))
)
);
if (shapes.tag === "Error") {
return (
<ErrorAlert heading="Distribution Error">
{shapes.value.toString()}
</ErrorAlert>
);
}
// if this is a sample set, include the samples
const samples: number[] = [];
for (const { distribution } of plot?.distributions) {
if (distribution.tag === SqDistributionTag.SampleSet) {
samples.push(...distribution.value());
}
}
const domain = shapes.value.flatMap((shape) =>
shape.discrete.concat(shape.continuous)
);
const spec = buildVegaSpec({
...props,
minX: props.minX ?? Math.min(...domain.map((x) => x.x)),
maxX: props.minX ?? Math.max(...domain.map((x) => x.x)),
maxY: Math.max(...domain.map((x) => x.y)),
});
// I think size.width is sometimes not finite due to the component not being in a visible context
// This occurs during testing
let widthProp = width
? width
: Number.isFinite(size.width)
? size.width
: 400;
if (widthProp < 20) {
console.warn(
`Width of Distribution is set to ${widthProp}, which is too small`
);
widthProp = 20;
}
const vegaData = { data: shapes.value, samples };
return (
<div style={{ width: widthProp }}>
{logX && shapes.value.some(hasMassBelowZero) ? (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
) : (
<Vega
spec={spec}
data={vegaData}
width={widthProp - 10}
height={height}
actions={actions}
/>
)}
<div className="flex justify-center">
{showSummary && plot.distributions.length === 1 && (
<SummaryTable
distribution={plot.distributions[0].distribution}
environment={environment}
/>
)}
</div>
</div>
);
});
return sized;
};
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<th className="border border-slate-200 bg-slate-50 py-1 px-2 text-slate-500 font-semibold">
{children}
</th>
);
const Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<td className="border border-slate-200 py-1 px-2 text-slate-900">
{children}
</td>
);
type SummaryTableProps = {
distribution: SqDistribution;
environment: environment;
};
const SummaryTable: React.FC<SummaryTableProps> = ({
distribution,
environment,
}) => {
const mean = distribution.mean(environment);
const stdev = distribution.stdev(environment);
const p5 = distribution.inv(environment, 0.05);
const p10 = distribution.inv(environment, 0.1);
const p25 = distribution.inv(environment, 0.25);
const p50 = distribution.inv(environment, 0.5);
const p75 = distribution.inv(environment, 0.75);
const p90 = distribution.inv(environment, 0.9);
const p95 = distribution.inv(environment, 0.95);
const hasResult = (x: result<number, SqDistributionError>): boolean =>
x.tag === "Ok";
const unwrapResult = (
x: result<number, SqDistributionError>
): React.ReactNode => {
if (x.tag === "Ok") {
return <NumberShower number={x.value} />;
} else {
return (
<ErrorAlert heading="Distribution Error">
{x.value.toString()}
</ErrorAlert>
);
}
};
return (
<table className="border border-collapse border-slate-400">
<thead className="bg-slate-50">
<tr>
<TableHeadCell>{"Mean"}</TableHeadCell>
{hasResult(stdev) && <TableHeadCell>{"Stdev"}</TableHeadCell>}
<TableHeadCell>{"5%"}</TableHeadCell>
<TableHeadCell>{"10%"}</TableHeadCell>
<TableHeadCell>{"25%"}</TableHeadCell>
<TableHeadCell>{"50%"}</TableHeadCell>
<TableHeadCell>{"75%"}</TableHeadCell>
<TableHeadCell>{"90%"}</TableHeadCell>
<TableHeadCell>{"95%"}</TableHeadCell>
</tr>
</thead>
<tbody>
<tr>
<Cell>{unwrapResult(mean)}</Cell>
{hasResult(stdev) && <Cell>{unwrapResult(stdev)}</Cell>}
<Cell>{unwrapResult(p5)}</Cell>
<Cell>{unwrapResult(p10)}</Cell>
<Cell>{unwrapResult(p25)}</Cell>
<Cell>{unwrapResult(p50)}</Cell>
<Cell>{unwrapResult(p75)}</Cell>
<Cell>{unwrapResult(p90)}</Cell>
<Cell>{unwrapResult(p95)}</Cell>
</tr>
</tbody>
</table>
);
};

View File

@ -1,80 +0,0 @@
import type { LogScale, LinearScale, PowScale } from "vega";
export let linearXScale: LinearScale = {
name: "xscale",
type: "linear",
range: "width",
zero: false,
nice: false,
domain: {
fields: [
{
data: "con",
field: "x",
},
{
data: "dis",
field: "x",
},
],
},
};
export let linearYScale: LinearScale = {
name: "yscale",
type: "linear",
range: "height",
zero: true,
domain: {
fields: [
{
data: "con",
field: "y",
},
{
data: "dis",
field: "y",
},
],
},
};
export let logXScale: LogScale = {
name: "xscale",
type: "log",
range: "width",
zero: false,
base: 10,
nice: false,
domain: {
fields: [
{
data: "con",
field: "x",
},
{
data: "dis",
field: "x",
},
],
},
};
export let expYScale: PowScale = {
name: "yscale",
type: "pow",
exponent: 0.1,
range: "height",
zero: true,
nice: false,
domain: {
fields: [
{
data: "con",
field: "y",
},
{
data: "dis",
field: "y",
},
],
},
};

View File

@ -0,0 +1,20 @@
import * as React from "react";
import styled from "styled-components";
const ShowError = styled.div`
border: 1px solid #792e2e;
background: #eee2e2;
padding: 0.4em 0.8em;
`;
export const Error: React.FC<{ heading: string; children: React.ReactNode }> = ({
heading = "Error",
children,
}) => {
return (
<ShowError>
<h3>{heading}</h3>
{children}
</ShowError>
);
};

View File

@ -1,107 +1,188 @@
import * as React from "react"; import * as React from "react";
import { import _ from "lodash";
SqLambda, import type { Spec } from "vega";
environment, import type { DistPlus } from "@quri/squiggle-lang";
SqValueTag, import { createClassFromSpec } from "react-vega";
SqError, import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
} from "@quri/squiggle-lang"; import { DistPlusChart } from "./DistPlusChart";
import { FunctionChart1Dist } from "./FunctionChart1Dist"; import { Error } from "./Error";
import { FunctionChart1Number } from "./FunctionChart1Number";
import { DistributionPlottingSettings } from "./DistributionChart";
import { MessageAlert } from "./Alert";
import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
export type FunctionChartSettings = { let SquigglePercentilesChart = createClassFromSpec({
start: number; spec: percentilesSpec as Spec,
stop: number; });
count: number;
type distPlusFn = (
a: number
) => { tag: "Ok"; value: DistPlus } | { tag: "Error"; value: string };
const _rangeByCount = (start, stop, count) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
}; };
interface FunctionChartProps { export const FunctionChart: React.FC<{
fn: SqLambda; distPlusFn: distPlusFn;
chartSettings: FunctionChartSettings; diagramStart: number;
distributionPlotSettings: DistributionPlottingSettings; diagramStop: number;
environment: environment; diagramCount: number;
height: number; }> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => {
} let [mouseOverlay, setMouseOverlay] = React.useState(NaN);
function handleHover(...args) {
const FunctionCallErrorAlert = ({ error }: { error: SqError }) => { setMouseOverlay(args[1]);
const [expanded, setExpanded] = React.useState(false);
if (expanded) {
} }
function handleOut(...args) {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let percentileArray = [
0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99,
];
let mouseItem = distPlusFn(mouseOverlay);
let showChart =
mouseItem.tag === "Ok" ? (
<DistPlusChart distPlus={mouseItem.value} width={400} height={140} />
) : (
<></>
);
let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount);
let valueData = data1
.map((x) => {
let result = distPlusFn(x);
if (result.tag === "Ok") {
return { x: x, value: result.value };
} else return null;
})
.filter((x) => x !== null)
.map(({ x, value }) => {
let percentiles = getPercentiles(percentileArray, value);
return {
x: x,
p1: percentiles[0],
p5: percentiles[1],
p10: percentiles[2],
p20: percentiles[3],
p30: percentiles[4],
p40: percentiles[5],
p50: percentiles[6],
p60: percentiles[7],
p70: percentiles[8],
p80: percentiles[9],
p90: percentiles[10],
p95: percentiles[11],
p99: percentiles[12],
};
});
let errorData = data1
.map((x) => {
let result = distPlusFn(x);
if (result.tag === "Error") {
return { x: x, error: result.value };
} else return null;
})
.filter((x) => x !== null);
let error2 = _.groupBy(errorData, (x) => x.error);
return ( return (
<MessageAlert heading="Function Display Failed"> <>
<div className="space-y-2"> <SquigglePercentilesChart
<span data={{ facet: valueData }}
className="underline decoration-dashed cursor-pointer" actions={false}
onClick={() => setExpanded(!expanded)} signalListeners={signalListeners}
> />
{expanded ? "Hide" : "Show"} error details {showChart}
</span> {_.keysIn(error2).map((k) => (
{expanded ? <SquiggleErrorAlert error={error} /> : null} <Error heading={k}>
</div> {`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`}
</MessageAlert> </Error>
))}
</>
); );
}; };
export const FunctionChart: React.FC<FunctionChartProps> = ({ function getPercentiles(percentiles: number[], t: DistPlus) {
fn, if (t.pointSetDist.tag === "Discrete") {
chartSettings, let total = 0;
environment, let maxX = _.max(t.pointSetDist.value.xyShape.xs);
distributionPlotSettings, let bounds = percentiles.map((_) => maxX);
height, _.zipWith(
}) => { t.pointSetDist.value.xyShape.xs,
console.log(fn.parameters().length); t.pointSetDist.value.xyShape.ys,
if (fn.parameters().length !== 1) { (x, y) => {
return ( total += y;
<MessageAlert heading="Function Display Not Supported"> percentiles.forEach((v, i) => {
Only functions with one parameter are displayed. if (total > v && bounds[i] === maxX) {
</MessageAlert> bounds[i] = x;
}
});
}
); );
} return bounds;
const result1 = fn.call([chartSettings.start]); } else if (t.pointSetDist.tag === "Continuous") {
const result2 = fn.call([chartSettings.stop]); let total = 0;
const getValidResult = () => { let maxX = _.max(t.pointSetDist.value.xyShape.xs);
if (result1.tag === "Ok") { let totalY = _.sum(t.pointSetDist.value.xyShape.ys);
return result1; let bounds = percentiles.map((_) => maxX);
} else if (result2.tag === "Ok") { _.zipWith(
return result2; t.pointSetDist.value.xyShape.xs,
} else { t.pointSetDist.value.xyShape.ys,
return result1; (x, y) => {
total += y / totalY;
percentiles.forEach((v, i) => {
if (total > v && bounds[i] === maxX) {
bounds[i] = x;
}
});
}
);
return bounds;
} else if (t.pointSetDist.tag === "Mixed") {
let discreteShape = t.pointSetDist.value.discrete.xyShape;
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
let continuousShape = t.pointSetDist.value.continuous.xyShape;
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
interface labeledPoint {
x: number;
y: number;
type: "discrete" | "continuous";
} }
};
const validResult = getValidResult();
if (validResult.tag === "Error") { let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({
return <FunctionCallErrorAlert error={validResult.value} />; x: x,
} y: y,
type: "discrete",
}));
let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({
x: x,
y: y,
type: "continuous",
}));
switch (validResult.value.tag) { let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x");
case SqValueTag.Distribution:
return ( let totalContinuous = 1 - totalDiscrete;
<FunctionChart1Dist let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b);
fn={fn}
chartSettings={chartSettings} let total = 0;
environment={environment} let maxX = _.max(sortedPoints.map((x) => x.x));
height={height} let bounds = percentiles.map((_) => maxX);
distributionPlotSettings={distributionPlotSettings} sortedPoints.map((point: labeledPoint) => {
/> if (point.type === "discrete") {
); total += point.y;
case SqValueTag.Number: } else if (point.type === "continuous") {
return ( total += (point.y / totalY) * totalContinuous;
<FunctionChart1Number }
fn={fn} percentiles.forEach((v, i) => {
chartSettings={chartSettings} if (total > v && bounds[i] === maxX) {
environment={environment} bounds[i] = total;
height={height} }
/> });
); return total;
default: });
return ( return bounds;
<MessageAlert heading="Function Display Not Supported">
There is no function visualization for this type of output:{" "}
<span className="font-bold">{validResult.value.tag}</span>
</MessageAlert>
);
} }
}; }

View File

@ -1,225 +0,0 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import {
SqDistribution,
result,
SqLambda,
environment,
SqError,
SqValue,
SqValueTag,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
import {
DistributionChart,
DistributionPlottingSettings,
defaultPlot,
} from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorAlert } from "./Alert";
let SquigglePercentilesChart = createClassFromSpec({
spec: percentilesSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
function unwrap<a, b>(x: result<a, b>): a {
if (x.tag === "Ok") {
return x.value;
} else {
throw Error("FAILURE TO UNWRAP");
}
}
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
interface FunctionChart1DistProps {
fn: SqLambda;
chartSettings: FunctionChartSettings;
distributionPlotSettings: DistributionPlottingSettings;
environment: environment;
height: number;
}
type percentiles = {
x: number;
p1: number;
p5: number;
p10: number;
p20: number;
p30: number;
p40: number;
p50: number;
p60: number;
p70: number;
p80: number;
p90: number;
p95: number;
p99: number;
}[];
type errors = _.Dictionary<
{
x: number;
value: string;
}[]
>;
type point = { x: number; value: result<SqDistribution, string> };
let getPercentiles = ({
chartSettings,
fn,
environment,
}: {
chartSettings: FunctionChartSettings;
fn: SqLambda;
environment: environment;
}) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = fn.call([x]);
if (result.tag === "Ok") {
if (result.value.tag === SqValueTag.Distribution) {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: result.value.toString() },
};
}
});
let initialPartition: [
{ x: number; value: SqDistribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => {
const res = {
x: x,
p1: unwrap(value.inv(environment, 0.01)),
p5: unwrap(value.inv(environment, 0.05)),
p10: unwrap(value.inv(environment, 0.1)),
p20: unwrap(value.inv(environment, 0.2)),
p30: unwrap(value.inv(environment, 0.3)),
p40: unwrap(value.inv(environment, 0.4)),
p50: unwrap(value.inv(environment, 0.5)),
p60: unwrap(value.inv(environment, 0.6)),
p70: unwrap(value.inv(environment, 0.7)),
p80: unwrap(value.inv(environment, 0.8)),
p90: unwrap(value.inv(environment, 0.9)),
p95: unwrap(value.inv(environment, 0.95)),
p99: unwrap(value.inv(environment, 0.99)),
};
return res;
});
return { percentiles, errors: groupedErrors };
};
export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
fn,
chartSettings,
environment,
distributionPlotSettings,
height,
}) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0);
function handleHover(_name: string, value: unknown) {
setMouseOverlay(value as number);
}
function handleOut() {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
//TODO: This custom error handling is a bit hacky and should be improved.
let mouseItem: result<SqValue, SqError> = !!mouseOverlay
? fn.call([mouseOverlay])
: {
tag: "Error",
value: SqError.createOtherError(
"Hover x-coordinate returned NaN. Expected a number."
),
};
let showChart =
mouseItem.tag === "Ok" &&
mouseItem.value.tag === SqValueTag.Distribution ? (
<DistributionChart
plot={defaultPlot(mouseItem.value.value)}
environment={environment}
width={400}
height={50}
{...distributionPlotSettings}
/>
) : null;
let getPercentilesMemoized = React.useMemo(
() => getPercentiles({ chartSettings, fn, environment }),
[environment, fn]
);
return (
<>
<SquigglePercentilesChart
data={{ facet: getPercentilesMemoized.percentiles }}
height={height}
actions={false}
signalListeners={signalListeners}
/>
{showChart}
{_.entries(getPercentilesMemoized.errors).map(
([errorName, errorPoints]) => (
<ErrorAlert key={errorName} heading={errorName}>
Values:{" "}
{errorPoints
.map((r, i) => <NumberShower key={i} number={r.x} />)
.reduce((a, b) => (
<>
{a}, {b}
</>
))}
</ErrorAlert>
)
)}
</>
);
};

View File

@ -1,119 +0,0 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import { result, SqLambda, environment, SqValueTag } from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as lineChartSpec from "../vega-specs/spec-line-chart.json";
import { ErrorAlert } from "./Alert";
import { squiggleValueTag } from "@quri/squiggle-lang/src/rescript/ForTS/ForTS_SquiggleValue/ForTS_SquiggleValue_tag";
let SquiggleLineChart = createClassFromSpec({
spec: lineChartSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
interface FunctionChart1NumberProps {
fn: SqLambda;
chartSettings: FunctionChartSettings;
environment: environment;
height: number;
}
type point = { x: number; value: result<number, string> };
let getFunctionImage = ({
chartSettings,
fn,
environment,
}: {
chartSettings: FunctionChartSettings;
fn: SqLambda;
environment: environment;
}) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = fn.call([x]);
if (result.tag === "Ok") {
if (result.value.tag === SqValueTag.Number) {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value: "This component expected number outputs",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: result.value.toString() },
};
}
});
let initialPartition: [
{ x: number; value: number }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
return { errors, functionImage };
};
export const FunctionChart1Number: React.FC<FunctionChart1NumberProps> = ({
fn,
chartSettings,
environment,
height,
}: FunctionChart1NumberProps) => {
let getFunctionImageMemoized = React.useMemo(
() => getFunctionImage({ chartSettings, fn, environment }),
[environment, fn]
);
let data = getFunctionImageMemoized.functionImage.map(({ x, value }) => ({
x,
y: value,
}));
return (
<>
<SquiggleLineChart
data={{ facet: data }}
height={height}
actions={false}
/>
{getFunctionImageMemoized.errors.map(({ x, value }) => (
<ErrorAlert key={x} heading={value}>
Error at point ${x}
</ErrorAlert>
))}
</>
);
};

View File

@ -1,49 +0,0 @@
import _ from "lodash";
import React, { FC } from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
oneLine?: boolean;
width?: number;
height: number;
showGutter?: boolean;
}
export const JsonEditor: FC<CodeEditorProps> = ({
value,
onChange,
oneLine = false,
showGutter = false,
height,
}) => {
const lineCount = value.split("\n").length;
const id = _.uniqueId();
return (
<AceEditor
value={value}
mode="json"
theme="github"
width={"100%"}
height={String(height) + "px"}
minLines={oneLine ? lineCount : undefined}
maxLines={oneLine ? lineCount : undefined}
showGutter={showGutter}
highlightActiveLine={false}
showPrintMargin={false}
onChange={onChange}
name={id}
editorProps={{
$blockScrolling: true,
}}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
}}
/>
);
};

View File

@ -1,95 +1,98 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
const orderOfMagnitudeNum = (n: number) => { const orderOfMagnitudeNum = (n: number) => {
return Math.pow(10, n); return Math.pow(10, n);
}; };
// 105 -> 3 // 105 -> 3
const orderOfMagnitude = (n: number) => { const orderOfMagnitude = (n: number) => {
return Math.floor(Math.log(n) / Math.LN10 + 0.000000001); return Math.floor(Math.log(n) / Math.LN10 + 0.000000001);
}; };
function withXSigFigs(number: number, sigFigs: number) { function withXSigFigs(number: number, sigFigs: number) {
const withPrecision = number.toPrecision(sigFigs); const withPrecision = number.toPrecision(sigFigs);
const formatted = Number(withPrecision); const formatted = Number(withPrecision);
return `${formatted}`; return `${formatted}`;
} }
class NumberShowerBuilder { class NumberShowerBuilder {
number: number; number: number;
precision: number; precision: number;
constructor(number: number, precision = 2) { constructor(number: number, precision = 2) {
this.number = number; this.number = number;
this.precision = precision; this.precision = precision;
}
convert() {
const number = Math.abs(this.number);
const response = this.evaluate(number);
if (this.number < 0) {
response.value = "-" + response.value;
}
return response;
}
metricSystem(number: number, order: number) {
const newNumber = number / orderOfMagnitudeNum(order);
const precision = this.precision;
return `${withXSigFigs(newNumber, precision)}`;
}
evaluate(number: number) {
if (number === 0) {
return { value: this.metricSystem(0, 0) };
} }
const order = orderOfMagnitude(number); convert() {
if (order < -2) { const number = Math.abs(this.number);
return { value: this.metricSystem(number, order), power: order }; const response = this.evaluate(number);
} else if (order < 4) { if (this.number < 0) {
return { value: this.metricSystem(number, 0) }; response.value = "-" + response.value;
} else if (order < 6) { }
return { value: this.metricSystem(number, 3), symbol: "K" }; return response;
} else if (order < 9) { }
return { value: this.metricSystem(number, 6), symbol: "M" };
} else if (order < 12) { metricSystem(number: number, order: number) {
return { value: this.metricSystem(number, 9), symbol: "B" }; const newNumber = number / orderOfMagnitudeNum(order);
} else if (order < 15) { const precision = this.precision;
return { value: this.metricSystem(number, 12), symbol: "T" }; return `${withXSigFigs(newNumber, precision)}`;
} else { }
return { value: this.metricSystem(number, order), power: order };
evaluate(number: number) {
if (number === 0) {
return { value: this.metricSystem(0, 0) };
}
const order = orderOfMagnitude(number);
if (order < -2) {
return { value: this.metricSystem(number, order), power: order };
} else if (order < 4) {
return { value: this.metricSystem(number, 0) };
} else if (order < 6) {
return { value: this.metricSystem(number, 3), symbol: "K" };
} else if (order < 9) {
return { value: this.metricSystem(number, 6), symbol: "M" };
} else if (order < 12) {
return { value: this.metricSystem(number, 9), symbol: "B" };
} else if (order < 15) {
return { value: this.metricSystem(number, 12), symbol: "T" };
} else {
return { value: this.metricSystem(number, order), power: order };
}
} }
}
} }
export function numberShow(number: number, precision = 2) { export function numberShow(number: number, precision = 2) {
const ns = new NumberShowerBuilder(number, precision); const ns = new NumberShowerBuilder(number, precision);
return ns.convert(); return ns.convert();
} }
export interface NumberShowerProps { export interface NumberShowerProps {
number: number; number: number;
precision?: number; precision?: number
} }
export const NumberShower: React.FC<NumberShowerProps> = ({ export let NumberShower: React.FC<NumberShowerProps> = ({
number, number,
precision = 2, precision = 2
}) => { }: NumberShowerProps) => {
const numberWithPresentation = numberShow(number, precision); let numberWithPresentation = numberShow(number, precision);
return ( return (
<span>
{numberWithPresentation.value}
{numberWithPresentation.symbol}
{numberWithPresentation.power ? (
<span> <span>
{"\u00b7" /* dot symbol */}10 {numberWithPresentation.value}
<span style={{ fontSize: "0.6em", verticalAlign: "super" }}> {numberWithPresentation.symbol}
{numberWithPresentation.power} {numberWithPresentation.power ? (
</span> <span>
{"\u00b710"}
<span style={{ fontSize: "0.6em", verticalAlign: "super" }}>
{numberWithPresentation.power}
</span>
</span>
) : (
<></>
)}
</span> </span>
) : null} );
</span> }
);
};

View File

@ -1,158 +1,91 @@
import * as React from "react"; import * as React from "react";
import { import _ from "lodash";
SqValue, import { run } from "@quri/squiggle-lang";
environment, import type {
SqProject, SamplingInputs,
defaultEnvironment, exportEnv,
exportDistribution,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useSquiggle } from "../lib/hooks"; import { NumberShower } from "./NumberShower";
import { SquiggleViewer } from "./SquiggleViewer"; import { DistPlusChart } from "./DistPlusChart";
import { JsImports } from "../lib/jsImports"; import { FunctionChart } from "./FunctionChart";
import { getValueToRender } from "../lib/utility"; import { Error } from "./Error";
export type SquiggleChartProps = { export interface SquiggleChartProps {
/** The input string for squiggle */ /** The input string for squiggle */
code: string; squiggleString?: string;
/** Allows to re-run the code if code hasn't changed */
executionId?: number;
/** If the output requires monte carlo sampling, the amount of samples */ /** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number; sampleCount?: number;
/** If the result is a function, where the function domain starts */ /** The amount of points returned to draw the distribution */
outputXYPoints?: number;
kernelWidth?: number;
pointDistLength?: number;
/** If the result is a function, where the function starts */
diagramStart?: number; diagramStart?: number;
/** If the result is a function, where the function domain ends */ /** If the result is a function, where the function ends */
diagramStop?: number; diagramStop?: number;
/** If the result is a function, the amount of stops sampled */ /** If the result is a function, how many points along the function it samples */
diagramCount?: number; diagramCount?: number;
/** When the squiggle code gets reevaluated */ /** variables declared before this expression */
onChange?(expr: SqValue | undefined, sourceName: string): void; environment?: exportEnv;
/** When the environment changes */
onEnvChange?(env: exportEnv): void;
/** CSS width of the element */ /** CSS width of the element */
width?: number; width?: number;
height?: number; height?: number;
/** JS imported parameters */ }
jsImports?: JsImports;
/** Whether to show a summary of the distribution */
showSummary?: boolean;
/** Set the x scale to be logarithmic by deault */
logX?: boolean;
/** Set the y scale to be exponential by deault */
expY?: boolean;
/** How to format numbers on the x axis */
tickFormat?: string;
/** Title of the graphed distribution */
title?: string;
/** Color of the graphed distribution */
color?: string;
/** Specify the lower bound of the x scale */
minX?: number;
/** Specify the upper bound of the x scale */
maxX?: number;
/** Whether the x-axis should be dates or numbers */
xAxisType?: "number" | "dateTime";
/** Whether to show vega actions to the user, so they can copy the chart spec */
distributionChartActions?: boolean;
enableLocalSettings?: boolean;
} & (StandaloneExecutionProps | ProjectExecutionProps);
// Props needed for a standalone execution export const SquiggleChart: React.FC<SquiggleChartProps> = ({
type StandaloneExecutionProps = { squiggleString = "",
project?: undefined; sampleCount = 1000,
continues?: undefined; outputXYPoints = 1000,
/** The amount of points returned to draw the distribution, not needed if using a project */ kernelWidth,
environment?: environment; pointDistLength = 1000,
}; diagramStart = 0,
diagramStop = 10,
// Props needed when executing inside a project. diagramCount = 20,
type ProjectExecutionProps = { environment = [],
environment?: undefined; onEnvChange = () => {},
/** The project that this execution is part of */ width = 500,
project: SqProject; height = 60,
/** What other squiggle sources from the project to continue. Default [] */ }: SquiggleChartProps) => {
continues?: string[]; let samplingInputs: SamplingInputs = {
}; sampleCount: sampleCount,
const defaultOnChange = () => {}; outputXYPoints: outputXYPoints,
const defaultImports: JsImports = {}; kernelWidth: kernelWidth,
pointDistLength: pointDistLength,
export const splitSquiggleChartSettings = (props: SquiggleChartProps) => {
const {
showSummary = false,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 20,
tickFormat,
minX,
maxX,
color,
title,
xAxisType = "number",
distributionChartActions,
} = props;
const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
xAxisType,
actions: distributionChartActions,
}; };
const chartSettings = { let result = run(squiggleString, samplingInputs, environment);
start: diagramStart, if (result.tag === "Ok") {
stop: diagramStop, let environment = result.value.environment;
count: diagramCount, let exports = result.value.exports;
}; onEnvChange(environment);
let chartResults = exports.map((chartResult: exportDistribution) => {
return { distributionPlotSettings, chartSettings }; if (chartResult["NAME"] === "Float") {
}; return <NumberShower precision={3} number={chartResult["VAL"]} />;
} else if (chartResult["NAME"] === "DistPlus") {
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo( return (
(props) => { <DistPlusChart
const { distributionPlotSettings, chartSettings } = distPlus={chartResult.VAL}
splitSquiggleChartSettings(props); height={height}
width={width}
const { />
code, );
jsImports = defaultImports, } else if (chartResult.NAME === "Function") {
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here return (
executionId = 0, <FunctionChart
width, distPlusFn={chartResult.VAL}
height = 200, diagramStart={diagramStart}
enableLocalSettings = false, diagramStop={diagramStop}
continues, diagramCount={diagramCount}
project, />
environment, );
} = props; }
const resultAndBindings = useSquiggle({
environment,
continues,
project,
code,
jsImports,
onChange,
executionId,
}); });
return <>{chartResults}</>;
const valueToRender = getValueToRender(resultAndBindings); } else if (result.tag === "Error") {
// At this point, we came across an error. What was our error?
return ( return <Error heading={"Parse Error"}>{result.value}</Error>;
<SquiggleViewer
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={
project ? project.getEnvironment() : environment ?? defaultEnvironment
}
enableLocalSettings={enableLocalSettings}
/>
);
} }
); };

View File

@ -1,26 +0,0 @@
import React, { useContext } from "react";
type Props = {
children: React.ReactNode;
};
type SquiggleContextShape = {
containerized: boolean;
};
const SquiggleContext = React.createContext<SquiggleContextShape>({
containerized: false,
});
export const SquiggleContainer: React.FC<Props> = ({ children }) => {
const context = useContext(SquiggleContext);
if (context.containerized) {
return <>{children}</>;
} else {
return (
<SquiggleContext.Provider value={{ containerized: true }}>
<div className="squiggle">{children}</div>
</SquiggleContext.Provider>
);
}
};

View File

@ -1,92 +1,107 @@
import React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { SquiggleContainer } from "./SquiggleContainer"; import type { exportEnv } from "@quri/squiggle-lang";
import { import styled from 'styled-components'
splitSquiggleChartSettings,
SquiggleChartProps,
} from "./SquiggleChart";
import { useMaybeControlledValue, useSquiggle } from "../lib/hooks";
import { JsImports } from "../lib/jsImports";
import { defaultEnvironment, SqLocation, SqProject } from "@quri/squiggle-lang";
import { SquiggleViewer } from "./SquiggleViewer";
import { getErrorLocations, getValueToRender } from "../lib/utility";
const WrappedCodeEditor: React.FC<{ export interface SquiggleEditorProps {
code: string; /** The input string for squiggle */
setCode: (code: string) => void; initialSquiggleString?: string;
errorLocations?: SqLocation[]; /** If the output requires monte carlo sampling, the amount of samples */
}> = ({ code, setCode, errorLocations }) => ( sampleCount?: number;
<div className="border border-grey-200 p-2 m-4" data-testid="squiggle-editor"> /** The amount of points returned to draw the distribution */
<CodeEditor outputXYPoints?: number;
value={code} kernelWidth?: number;
onChange={setCode} pointDistLength?: number;
oneLine={true} /** If the result is a function, where the function starts */
showGutter={false} diagramStart?: number;
height={20} /** If the result is a function, where the function ends */
errorLocations={errorLocations} diagramStop?: number;
/> /** If the result is a function, how many points along the function it samples */
</div> diagramCount?: number;
); /** The environment, other variables that were already declared */
environment?: exportEnv;
/** when the environment changes. Used again for notebook magic*/
onEnvChange?(env: exportEnv): void;
/** The width of the element */
width: number;
}
export type SquiggleEditorProps = SquiggleChartProps & { const Input = styled.div`
defaultCode?: string; border: 1px solid #ddd;
onCodeChange?: (code: string) => void; padding: 0.3em 0.3em;
}; margin-bottom: 1em;
`;
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
const [code, setCode] = useMaybeControlledValue({
value: props.code,
defaultValue: props.defaultCode ?? "",
onChange: props.onCodeChange,
});
const { distributionPlotSettings, chartSettings } =
splitSquiggleChartSettings(props);
const {
environment,
jsImports = defaultImports,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
continues,
project,
} = props;
const resultAndBindings = useSquiggle({
environment,
continues,
code,
project,
jsImports,
onChange,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings);
const errorLocations = getErrorLocations(resultAndBindings.result);
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
initialSquiggleString = "",
width = 500,
sampleCount,
outputXYPoints,
kernelWidth,
pointDistLength,
diagramStart,
diagramStop,
diagramCount,
onEnvChange,
environment,
}: SquiggleEditorProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString);
return ( return (
<SquiggleContainer> <div>
<WrappedCodeEditor <Input>
code={code} <CodeEditor
setCode={setCode} value={expression}
errorLocations={errorLocations} onChange={setExpression}
/> oneLine={true}
<SquiggleViewer />
result={valueToRender} </Input>
<SquiggleChart
width={width} width={width}
height={height} squiggleString={expression}
distributionPlotSettings={distributionPlotSettings} sampleCount={sampleCount}
chartSettings={chartSettings} outputXYPoints={outputXYPoints}
environment={environment ?? defaultEnvironment} kernelWidth={kernelWidth}
enableLocalSettings={enableLocalSettings} pointDistLength={pointDistLength}
diagramStart={diagramStart}
diagramStop={diagramStop}
diagramCount={diagramCount}
environment={environment}
onEnvChange={onEnvChange}
/> />
</SquiggleContainer> </div>
); );
}; };
export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
let parent = document.createElement("div");
ReactDOM.render(
<SquiggleEditor
{...props}
onEnvChange={(env) => {
// Typescript complains on two levels here.
// - Div elements don't have a value property
// - Even if it did (like it was an input element), it would have to
// be a string
//
// Which are reasonable in most web contexts.
//
// However we're using observable, neither of those things have to be
// true there. div elements can contain the value property, and can have
// the value be any datatype they wish.
//
// This is here to get the 'viewof' part of:
// viewof env = cell('normal(0,1)')
// to work
// @ts-ignore
parent.value = env;
parent.dispatchEvent(new CustomEvent("input"));
if (props.onEnvChange) props.onEnvChange(env);
}}
/>,
parent
);
return parent;
}

View File

@ -1,44 +0,0 @@
import { SqError, SqFrame } from "@quri/squiggle-lang";
import React from "react";
import { ErrorAlert } from "./Alert";
type Props = {
error: SqError;
};
const StackTraceFrame: React.FC<{ frame: SqFrame }> = ({ frame }) => {
const location = frame.location();
return (
<div>
{frame.name()}
{location
? ` at line ${location.start.line}, column ${location.start.column}`
: ""}
</div>
);
};
const StackTrace: React.FC<Props> = ({ error }) => {
const frames = error.getFrameArray();
return frames.length ? (
<div>
<div className="font-medium">Stack trace:</div>
<div className="ml-4">
{frames.map((frame, i) => (
<StackTraceFrame frame={frame} key={i} />
))}
</div>
</div>
) : null;
};
export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => {
return (
<ErrorAlert heading="Error">
<div className="space-y-4">
<div>{error.toString()}</div>
<StackTrace error={error} />
</div>
</ErrorAlert>
);
};

View File

@ -1,450 +1,131 @@
import React, { import _ from "lodash";
FC, import React, { FC, useState } from "react";
useState, import ReactDOM from "react-dom";
useEffect, import { SquiggleChart } from "./SquiggleChart";
useMemo, import CodeEditor from "./CodeEditor";
useRef, import { Form, Input, Card, Row, Col } from "antd";
useCallback, import "antd/dist/antd.css";
} from "react";
import { useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup";
import {
useMaybeControlledValue,
useRunnerState,
useSquiggle,
} from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup";
import {
ChartSquareBarIcon,
CheckCircleIcon,
ClipboardCopyIcon,
CodeIcon,
CogIcon,
CurrencyDollarIcon,
EyeIcon,
PauseIcon,
PlayIcon,
RefreshIcon,
} from "@heroicons/react/solid";
import clsx from "clsx";
import { environment, SqProject } from "@quri/squiggle-lang"; interface FieldFloatProps {
label: string;
import { SquiggleChartProps } from "./SquiggleChart"; className?: string;
import { CodeEditor } from "./CodeEditor"; value: number;
import { JsonEditor } from "./JsonEditor"; onChange: (value: number) => void;
import { ErrorAlert, SuccessAlert } from "./Alert"; }
import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle";
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 { defaultTickFormat } from "../lib/distributionSpecBuilder";
import { Button } from "./ui/Button";
import { JsImports } from "../lib/jsImports";
import { getErrorLocations, getValueToRender } from "../lib/utility";
import { SquiggleViewer } from "./SquiggleViewer";
type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */
defaultCode?: string;
onCodeChange?(expr: string): void;
/* When settings change */
onSettingsChange?(settings: any): void;
/** Should we show the editor? */
showEditor?: boolean;
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
showShareButton?: boolean;
};
const schema = yup
.object({})
.shape({
sampleCount: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(1000000),
xyPointLength: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(10000),
})
.concat(viewSettingsSchema);
type FormFields = yup.InferType<typeof schema>;
const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register,
}) => (
<div className="space-y-6 p-3 max-w-xl">
<div>
<InputItem
name="sampleCount"
type="number"
label="Sample Count"
register={register}
/>
<div className="mt-2">
<Text>
How many samples to use for Monte Carlo simulations. This can
occasionally be overridden by specific Squiggle programs.
</Text>
</div>
</div>
<div>
<InputItem
name="xyPointLength"
type="number"
register={register}
label="Coordinate Count (For PointSet Shapes)"
/>
<div className="mt-2">
<Text>
When distributions are converted into PointSet shapes, we need to know
how many coordinates to use.
</Text>
</div>
</div>
</div>
);
const InputVariablesSettings: React.FC<{
initialImports: JsImports;
setImports: (imports: JsImports) => void;
}> = ({ initialImports, setImports }) => {
const [importString, setImportString] = useState(() =>
JSON.stringify(initialImports)
);
const [importsAreValid, setImportsAreValid] = useState(true);
const onChange = (value: string) => {
setImportString(value);
let imports = {};
try {
imports = JSON.parse(value);
setImportsAreValid(true);
} catch (e) {
setImportsAreValid(false);
}
setImports(imports);
};
function FieldFloat(Props: FieldFloatProps) {
let [contents, setContents] = useState(Props.value + "");
return ( return (
<div className="p-3 max-w-3xl"> <Form.Item label={Props.label}>
<HeadedSection title="Import Variables from JSON"> <Input
<div className="space-y-6"> value={contents}
<Text> className={Props.className ? Props.className : ""}
You can import variables from JSON into your Squiggle code. onChange={(e) => {
Variables are accessed with dollar signs. For example, "timeNow" setContents(e.target.value);
would be accessed as "$timeNow". let result = parseFloat(contents);
</Text> if (_.isFinite(result)) {
<div className="border border-slate-200 mt-6 mb-2"> Props.onChange(result);
<JsonEditor
value={importString}
onChange={onChange}
oneLine={false}
showGutter={true}
height={150}
/>
</div>
<div className="p-1 pt-2">
{importsAreValid ? (
<SuccessAlert heading="Valid JSON" />
) : (
<ErrorAlert heading="Invalid JSON">
You must use valid JSON in this editor.
</ErrorAlert>
)}
</div>
</div>
</HeadedSection>
</div>
);
};
const RunControls: React.FC<{
autorunMode: boolean;
isRunning: boolean;
isStale: boolean;
onAutorunModeChange: (value: boolean) => void;
run: () => void;
}> = ({ autorunMode, isRunning, isStale, onAutorunModeChange, run }) => {
const CurrentPlayIcon = isRunning ? RefreshIcon : PlayIcon;
return (
<div className="flex space-x-1 items-center" data-testid="autorun-controls">
{autorunMode ? null : (
<button onClick={run}>
<CurrentPlayIcon
className={clsx(
"w-8 h-8",
isRunning && "animate-spin",
isStale ? "text-indigo-500" : "text-gray-400"
)}
/>
</button>
)}
<Toggle
texts={["Autorun", "Paused"]}
icons={[CheckCircleIcon, PauseIcon]}
status={autorunMode}
onChange={onAutorunModeChange}
spinIcon={autorunMode && isRunning}
/>
</div>
);
};
const ShareButton: React.FC = () => {
const [isCopied, setIsCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText((window.top || window).location.href);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
};
return (
<div className="w-36">
<Button onClick={copy} wide>
{isCopied ? (
"Copied to clipboard!"
) : (
<div className="flex items-center space-x-1">
<ClipboardCopyIcon className="w-4 h-4" />
<span>Copy share link</span>
</div>
)}
</Button>
</div>
);
};
type PlaygroundContextShape = {
getLeftPanelElement: () => HTMLDivElement | undefined;
};
export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
getLeftPanelElement: () => undefined,
});
export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "",
height = 500,
showSummary = true,
logX = false,
expY = false,
title,
minX,
maxX,
tickFormat = defaultTickFormat,
distributionChartActions,
code: controlledCode,
onCodeChange,
onSettingsChange,
showEditor = true,
showShareButton = false,
continues,
project,
}) => {
const [code, setCode] = useMaybeControlledValue({
value: controlledCode,
defaultValue: defaultCode,
onChange: onCodeChange,
});
const [imports, setImports] = useState<JsImports>({});
const { register, control } = useForm({
resolver: yupResolver(schema),
defaultValues: {
sampleCount: 1000,
xyPointLength: 1000,
chartHeight: 150,
logX,
expY,
title,
minX,
maxX,
tickFormat,
distributionChartActions,
showSummary,
showEditor,
diagramStart: 0,
diagramStop: 10,
diagramCount: 20,
},
});
const vars = useWatch({
control,
});
useEffect(() => {
onSettingsChange?.(vars);
}, [vars, onSettingsChange]);
const environment: environment = useMemo(
() => ({
sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength),
}),
[vars.sampleCount, vars.xyPointLength]
);
const {
run,
autorunMode,
setAutorunMode,
isRunning,
renderedCode,
executionId,
} = useRunnerState(code);
const resultAndBindings = useSquiggle({
environment,
continues,
code: renderedCode,
project,
jsImports: imports,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings);
const squiggleChart =
renderedCode === "" ? null : (
<div className="relative">
{isRunning ? (
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
) : null}
<SquiggleViewer
result={valueToRender}
environment={environment}
height={vars.chartHeight || 150}
distributionPlotSettings={{
showSummary: vars.showSummary ?? false,
logX: vars.logX ?? false,
expY: vars.expY ?? false,
format: vars.tickFormat,
minX: vars.minX,
maxX: vars.maxX,
title: vars.title,
actions: vars.distributionChartActions,
}}
chartSettings={{
start: vars.diagramStart ?? 0,
stop: vars.diagramStop ?? 10,
count: vars.diagramCount ?? 20,
}}
enableLocalSettings={true}
/>
</div>
);
const errorLocations = getErrorLocations(resultAndBindings.result);
const firstTab = vars.showEditor ? (
<div className="border border-slate-200" data-testid="squiggle-editor">
<CodeEditor
errorLocations={errorLocations}
value={code}
onChange={setCode}
onSubmit={run}
oneLine={false}
showGutter={true}
height={height - 1}
/>
</div>
) : (
squiggleChart
);
const tabs = (
<StyledTab.Panels>
<StyledTab.Panel>{firstTab}</StyledTab.Panel>
<StyledTab.Panel>
<SamplingSettings register={register} />
</StyledTab.Panel>
<StyledTab.Panel>
<ViewSettings
register={
// This is dangerous, but doesn't cause any problems.
// I tried to make `ViewSettings` generic (to allow it to accept any extension of a settings schema), but it didn't work.
register as unknown as UseFormRegister<
yup.InferType<typeof viewSettingsSchema>
>
} }
/> }}
</StyledTab.Panel> />
<StyledTab.Panel> </Form.Item>
<InputVariablesSettings
initialImports={imports}
setImports={setImports}
/>
</StyledTab.Panel>
</StyledTab.Panels>
); );
}
const leftPanelRef = useRef<HTMLDivElement | null>(null); interface Props {
initialSquiggleString?: string;
}
const withEditor = ( let SquigglePlayground: FC<Props> = ({initialSquiggleString=""}: Props) => {
<div className="flex mt-2"> let [squiggleString, setSquiggleString] = useState(
<div initialSquiggleString
className="w-1/2 relative" );
style={{ minHeight: height }} let [sampleCount, setSampleCount] = useState(1000);
ref={leftPanelRef} let [outputXYPoints, setOutputXYPoints] = useState(1000);
> let [pointDistLength, setPointDistLength] = useState(1000);
{tabs} let [diagramStart, setDiagramStart] = useState(0);
</div> let [diagramStop, setDiagramStop] = useState(10);
<div className="w-1/2 p-2 pl-4" data-testid="playground-result"> let [diagramCount, setDiagramCount] = useState(20);
{squiggleChart} var demoDist = (
</div> <SquiggleChart
</div> squiggleString={squiggleString}
sampleCount={sampleCount}
outputXYPoints={outputXYPoints}
diagramStart={diagramStart}
diagramStop={diagramStop}
diagramCount={diagramCount}
pointDistLength={pointDistLength}
height={150}
/>
); );
const withoutEditor = <div className="mt-3">{tabs}</div>;
const getLeftPanelElement = useCallback(() => {
return leftPanelRef.current ?? undefined;
}, []);
return ( return (
<SquiggleContainer> <Row>
<PlaygroundContext.Provider value={{ getLeftPanelElement }}> <Col span={12}>
<StyledTab.Group> <Card title="Distribution Form">
<div className="pb-4"> <Form>
<div className="flex justify-between items-center"> <Row gutter={16}>
<StyledTab.List> <Col span={24}>
<StyledTab <CodeEditor
name={vars.showEditor ? "Code" : "Display"} value={squiggleString}
icon={vars.showEditor ? CodeIcon : EyeIcon} onChange={setSquiggleString}
oneLine={false}
/> />
<StyledTab name="Sampling Settings" icon={CogIcon} /> </Col>
<StyledTab name="View Settings" icon={ChartSquareBarIcon} /> </Row>
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} /> <Row gutter={16}>
</StyledTab.List> <Col span={12}>
<div className="flex space-x-2 items-center"> <FieldFloat
<RunControls value={sampleCount}
autorunMode={autorunMode} label="Sample Count"
isStale={renderedCode !== code} onChange={setSampleCount}
run={run}
isRunning={isRunning}
onAutorunModeChange={setAutorunMode}
/> />
{showShareButton && <ShareButton />} </Col>
</div> <Col span={12}>
</div> <FieldFloat
{vars.showEditor ? withEditor : withoutEditor} value={outputXYPoints}
</div> onChange={setOutputXYPoints}
</StyledTab.Group> label="Output XY-points"
</PlaygroundContext.Provider> />
</SquiggleContainer> </Col>
<Col span={12}>
<FieldFloat
value={pointDistLength}
onChange={setPointDistLength}
label="Downsample To"
/>
</Col>
<Col span={12}>
<FieldFloat
value={diagramStart}
onChange={setDiagramStart}
label="Diagram Start"
/>
</Col>
<Col span={12}>
<FieldFloat
value={diagramStop}
onChange={setDiagramStop}
label="Diagram Stop"
/>
</Col>
<Col span={12}>
<FieldFloat
value={diagramCount}
onChange={setDiagramCount}
label="Diagram Count"
/>
</Col>
</Row>
</Form>
</Card>
</Col>
<Col span={12}>{demoDist}</Col>
</Row>
); );
}; };
export default SquigglePlayground;
export function renderSquigglePlaygroundToDom(props: Props) {
let parent = document.createElement("div");
ReactDOM.render(<SquigglePlayground {...props} />, parent);
return parent;
}

View File

@ -1,310 +0,0 @@
import React, { useContext } from "react";
import { SqDistributionTag, SqValue, SqValueTag } from "@quri/squiggle-lang";
import { NumberShower } from "../NumberShower";
import { DistributionChart, defaultPlot, makePlot } from "../DistributionChart";
import { FunctionChart } from "../FunctionChart";
import clsx from "clsx";
import { VariableBox } from "./VariableBox";
import { ItemSettingsMenu } from "./ItemSettingsMenu";
import { hasMassBelowZero } from "../../lib/distributionUtils";
import { MergedItemSettings } from "./utils";
import { ViewerContext } from "./ViewerContext";
/*
// DISABLED FOR 0.4 branch, for now
function getRange<a>(x: declaration<a>) {
const first = x.args[0];
switch (first.tag) {
case "Float": {
return { floats: { min: first.value.min, max: first.value.max } };
}
case "Date": {
return { time: { min: first.value.min, max: first.value.max } };
}
}
}
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
const range = getRange(x);
const min = range.floats ? range.floats.min : 0;
const max = range.floats ? range.floats.max : 10;
return {
start: min,
stop: max,
count: 20,
};
}
*/
const VariableList: React.FC<{
value: SqValue;
heading: string;
children: (settings: MergedItemSettings) => React.ReactNode;
}> = ({ value, heading, children }) => (
<VariableBox value={value} heading={heading}>
{(settings) => (
<div
className={clsx(
"space-y-3",
value.location.path.items.length ? "pt-1 mt-1" : null
)}
>
{children(settings)}
</div>
)}
</VariableBox>
);
export interface Props {
/** The output of squiggle's run */
value: SqValue;
width?: number;
}
export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
const { getMergedSettings } = useContext(ViewerContext);
switch (value.tag) {
case SqValueTag.Number:
return (
<VariableBox value={value} heading="Number">
{() => (
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={value.value} />
</div>
)}
</VariableBox>
);
case SqValueTag.Distribution: {
const distType = value.value.tag;
return (
<VariableBox
value={value}
heading={`Distribution (${distType})\n${
distType === SqDistributionTag.Symbolic
? value.value.toString()
: ""
}`}
renderSettingsMenu={({ onChange }) => {
const shape = value.value.pointSet(
getMergedSettings(value.location).environment
);
return (
<ItemSettingsMenu
value={value}
onChange={onChange}
disableLogX={
shape.tag === "Ok" && hasMassBelowZero(shape.value.asShape())
}
withFunctionSettings={false}
/>
);
}}
>
{(settings) => {
return (
<DistributionChart
plot={defaultPlot(value.value)}
environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
width={width}
/>
);
}}
</VariableBox>
);
}
case SqValueTag.String:
return (
<VariableBox value={value} heading="String">
{() => (
<>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold font-mono">
{value.value}
</span>
<span className="text-slate-400">"</span>
</>
)}
</VariableBox>
);
case SqValueTag.Bool:
return (
<VariableBox value={value} heading="Boolean">
{() => value.value.toString()}
</VariableBox>
);
case SqValueTag.Date:
return (
<VariableBox value={value} heading="Date">
{() => value.value.toDateString()}
</VariableBox>
);
case SqValueTag.Void:
return (
<VariableBox value={value} heading="Void">
{() => "Void"}
</VariableBox>
);
case SqValueTag.TimeDuration: {
return (
<VariableBox value={value} heading="Time Duration">
{() => <NumberShower precision={3} number={value.value} />}
</VariableBox>
);
}
case SqValueTag.Lambda:
return (
<VariableBox
value={value}
heading="Function"
renderSettingsMenu={({ onChange }) => {
return (
<ItemSettingsMenu
value={value}
onChange={onChange}
withFunctionSettings={true}
/>
);
}}
>
{(settings) => (
<>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${value.value
.parameters()
.join(",")})`}</div>
<FunctionChart
fn={value.value}
chartSettings={settings.chartSettings}
distributionPlotSettings={settings.distributionPlotSettings}
height={settings.height}
environment={{
sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10,
}}
/>
</>
)}
</VariableBox>
);
case SqValueTag.Declaration: {
return (
<VariableBox
value={value}
heading="Function Declaration"
renderSettingsMenu={({ onChange }) => {
return (
<ItemSettingsMenu
onChange={onChange}
value={value}
withFunctionSettings={true}
/>
);
}}
>
{(settings) => (
<div>NOT IMPLEMENTED IN 0.4 YET</div>
// <FunctionChart
// fn={expression.value.fn}
// chartSettings={getChartSettings(expression.value)}
// distributionPlotSettings={settings.distributionPlotSettings}
// height={settings.height}
// environment={{
// sampleCount: settings.environment.sampleCount / 10,
// xyPointLength: settings.environment.xyPointLength / 10,
// }}
// />
)}
</VariableBox>
);
}
case SqValueTag.Record:
const plot = makePlot(value.value);
if (plot) {
return (
<VariableBox
value={value}
heading="Plot"
renderSettingsMenu={({ onChange }) => {
let disableLogX = plot.distributions.some((x) => {
let pointSet = x.distribution.pointSet(
getMergedSettings(value.location).environment
);
return (
pointSet.tag === "Ok" &&
hasMassBelowZero(pointSet.value.asShape())
);
});
return (
<ItemSettingsMenu
value={value}
onChange={onChange}
disableLogX={disableLogX}
withFunctionSettings={false}
/>
);
}}
>
{(settings) => {
return (
<DistributionChart
plot={plot}
environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
width={width}
/>
);
}}
</VariableBox>
);
} else {
return (
<VariableList value={value} heading="Record">
{(_) =>
value.value
.entries()
.map(([key, r]) => (
<ExpressionViewer
key={key}
value={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
}
case SqValueTag.Array:
return (
<VariableList value={value} heading="Array">
{(_) =>
value.value
.getValues()
.map((r, i) => (
<ExpressionViewer
key={i}
value={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
default: {
return (
<VariableList value={value} heading="Error">
{() => (
<div>
<span>No display for type: </span>{" "}
<span className="font-semibold text-slate-600">
{(value as { tag: string }).tag}
</span>
</div>
)}
</VariableList>
);
}
}
};

View File

@ -1,164 +0,0 @@
import { CogIcon } from "@heroicons/react/solid";
import React, { useContext, useRef, useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Modal } from "../ui/Modal";
import { ViewSettings, viewSettingsSchema } from "../ViewSettings";
import { ViewerContext } from "./ViewerContext";
import { defaultTickFormat } from "../../lib/distributionSpecBuilder";
import { PlaygroundContext } from "../SquigglePlayground";
import { SqValue } from "@quri/squiggle-lang";
import { locationAsString } from "./utils";
type Props = {
value: SqValue;
onChange: () => void;
disableLogX?: boolean;
withFunctionSettings: boolean;
};
const ItemSettingsModal: React.FC<
Props & { close: () => void; resetScroll: () => void }
> = ({
value,
onChange,
disableLogX,
withFunctionSettings,
close,
resetScroll,
}) => {
const { setSettings, getSettings, getMergedSettings } =
useContext(ViewerContext);
const mergedSettings = getMergedSettings(value.location);
const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema),
defaultValues: {
// this is a mess and should be fixed
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,
minX: mergedSettings.distributionPlotSettings.minX,
maxX: mergedSettings.distributionPlotSettings.maxX,
distributionChartActions: mergedSettings.distributionPlotSettings.actions,
diagramStart: mergedSettings.chartSettings.start,
diagramStop: mergedSettings.chartSettings.stop,
diagramCount: mergedSettings.chartSettings.count,
},
});
useEffect(() => {
const subscription = watch((vars) => {
const settings = getSettings(value.location); // get the latest version
setSettings(value.location, {
...settings,
distributionPlotSettings: {
showSummary: vars.showSummary,
logX: vars.logX,
expY: vars.expY,
format: vars.tickFormat,
title: vars.title,
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, value.location, watch]);
const { getLeftPanelElement } = useContext(PlaygroundContext);
return (
<Modal container={getLeftPanelElement()} close={close}>
<Modal.Header>
Chart settings
{value.location.path.items.length ? (
<>
{" for "}
<span
title="Scroll to item"
className="cursor-pointer"
onClick={resetScroll}
>
{locationAsString(value.location)}
</span>{" "}
</>
) : (
""
)}
</Modal.Header>
<Modal.Body>
<ViewSettings
register={register}
withShowEditorSetting={false}
withFunctionSettings={withFunctionSettings}
disableLogXSetting={disableLogX}
/>
</Modal.Body>
</Modal>
);
};
export const ItemSettingsMenu: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const { enableLocalSettings, setSettings, getSettings } =
useContext(ViewerContext);
const ref = useRef<HTMLDivElement | null>(null);
if (!enableLocalSettings) {
return null;
}
const settings = getSettings(props.value.location);
const resetScroll = () => {
if (!ref.current) return;
window.scroll({
top: ref.current.getBoundingClientRect().y + window.scrollY,
behavior: "smooth",
});
};
return (
<div className="flex gap-2" ref={ref}>
<CogIcon
className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => setIsOpen(!isOpen)}
/>
{settings.distributionPlotSettings || settings.chartSettings ? (
<button
onClick={() => {
setSettings(props.value.location, {
...settings,
distributionPlotSettings: undefined,
chartSettings: undefined,
});
props.onChange();
}}
className="text-xs px-1 py-0.5 rounded bg-slate-300"
>
Reset settings
</button>
) : null}
{isOpen ? (
<ItemSettingsModal
{...props}
close={() => setIsOpen(false)}
resetScroll={resetScroll}
/>
) : null}
</div>
);
};

View File

@ -1,82 +0,0 @@
import { SqValue } from "@quri/squiggle-lang";
import React, { useContext, useReducer } from "react";
import { Tooltip } from "../ui/Tooltip";
import { LocalItemSettings, MergedItemSettings } from "./utils";
import { ViewerContext } from "./ViewerContext";
type SettingsMenuParams = {
onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself
};
type VariableBoxProps = {
value: SqValue;
heading: string;
renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode;
children: (settings: MergedItemSettings) => React.ReactNode;
};
export const VariableBox: React.FC<VariableBoxProps> = ({
value: { location },
heading = "Error",
renderSettingsMenu,
children,
}) => {
const { setSettings, getSettings, getMergedSettings } =
useContext(ViewerContext);
// Since ViewerContext doesn't keep the actual settings, VariableBox won't rerender when setSettings is called.
// So we use `forceUpdate` to force rerendering.
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
const settings = getSettings(location);
const setSettingsAndUpdate = (newSettings: LocalItemSettings) => {
setSettings(location, newSettings);
forceUpdate();
};
const toggleCollapsed = () => {
setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed });
};
const isTopLevel = location.path.items.length === 0;
const name = isTopLevel
? { result: "Result", bindings: "Bindings" }[location.path.root]
: location.path.items[location.path.items.length - 1];
return (
<div role={isTopLevel ? "status" : undefined}>
<header className="inline-flex space-x-1">
<Tooltip text={heading}>
<span
className="text-slate-500 font-mono text-sm cursor-pointer"
onClick={toggleCollapsed}
>
{name}:
</span>
</Tooltip>
{settings.collapsed ? (
<span
className="rounded p-0.5 bg-slate-200 text-slate-500 font-mono text-xs cursor-pointer"
onClick={toggleCollapsed}
>
...
</span>
) : renderSettingsMenu ? (
renderSettingsMenu({ onChange: forceUpdate })
) : null}
</header>
{settings.collapsed ? null : (
<div className="flex w-full">
{location.path.items.length ? (
<div
className="shrink-0 border-l-2 border-slate-200 hover:border-indigo-600 w-4 cursor-pointer"
onClick={toggleCollapsed}
></div>
) : null}
<div className="grow">{children(getMergedSettings(location))}</div>
</div>
)}
</div>
);
};

View File

@ -1,35 +0,0 @@
import { defaultEnvironment, SqValueLocation } from "@quri/squiggle-lang";
import React from "react";
import { LocalItemSettings, MergedItemSettings } from "./utils";
type ViewerContextShape = {
// Note that we don't store settings themselves in the context (that would cause rerenders of the entire tree on each settings update).
// Instead, we keep settings in local state and notify the global context via setSettings to pass them down the component tree again if it got rebuilt from scratch.
// See ./SquiggleViewer.tsx and ./VariableBox.tsx for other implementation details on this.
getSettings(location: SqValueLocation): LocalItemSettings;
getMergedSettings(location: SqValueLocation): MergedItemSettings;
setSettings(location: SqValueLocation, value: LocalItemSettings): void;
enableLocalSettings: boolean; // show local settings icon in the UI
};
export const ViewerContext = React.createContext<ViewerContextShape>({
getSettings: () => ({ collapsed: false }),
getMergedSettings: () => ({
collapsed: false,
// copy-pasted from SquiggleChart
chartSettings: {
start: 0,
stop: 10,
count: 100,
},
distributionPlotSettings: {
showSummary: false,
logX: false,
expY: false,
},
environment: defaultEnvironment,
height: 150,
}),
setSettings() {},
enableLocalSettings: false,
});

View File

@ -1,99 +0,0 @@
import React, { useCallback, useRef } from "react";
import { environment, SqValueLocation } from "@quri/squiggle-lang";
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { ExpressionViewer } from "./ExpressionViewer";
import { ViewerContext } from "./ViewerContext";
import {
LocalItemSettings,
locationAsString,
MergedItemSettings,
} from "./utils";
import { useSquiggle } from "../../lib/hooks";
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
type Props = {
/** The output of squiggle's run */
result: ReturnType<typeof useSquiggle>["result"];
width?: number;
height: number;
distributionPlotSettings: DistributionPlottingSettings;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
enableLocalSettings?: boolean;
};
type Settings = {
[k: string]: LocalItemSettings;
};
const defaultSettings: LocalItemSettings = { collapsed: false };
export const SquiggleViewer: React.FC<Props> = ({
result,
width,
height,
distributionPlotSettings,
chartSettings,
environment,
enableLocalSettings = false,
}) => {
// can't store settings in the state because we don't want to rerender the entire tree on every change
const settingsRef = useRef<Settings>({});
const getSettings = useCallback(
(location: SqValueLocation) => {
return settingsRef.current[locationAsString(location)] || defaultSettings;
},
[settingsRef]
);
const setSettings = useCallback(
(location: SqValueLocation, value: LocalItemSettings) => {
settingsRef.current[locationAsString(location)] = value;
},
[settingsRef]
);
const getMergedSettings = useCallback(
(location: SqValueLocation) => {
const localSettings = getSettings(location);
const result: MergedItemSettings = {
distributionPlotSettings: {
...distributionPlotSettings,
...(localSettings.distributionPlotSettings || {}),
},
chartSettings: {
...chartSettings,
...(localSettings.chartSettings || {}),
},
environment: {
...environment,
...(localSettings.environment || {}),
},
height: localSettings.height || height,
};
return result;
},
[distributionPlotSettings, chartSettings, environment, height, getSettings]
);
return (
<ViewerContext.Provider
value={{
getSettings,
setSettings,
getMergedSettings,
enableLocalSettings,
}}
>
{result.tag === "Ok" ? (
<ExpressionViewer value={result.value} width={width} />
) : (
<SquiggleErrorAlert error={result.value} />
)}
</ViewerContext.Provider>
);
};

View File

@ -1,21 +0,0 @@
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { environment, SqValueLocation } from "@quri/squiggle-lang";
export type LocalItemSettings = {
collapsed: boolean;
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
chartSettings?: Partial<FunctionChartSettings>;
height?: number;
environment?: Partial<environment>;
};
export type MergedItemSettings = {
distributionPlotSettings: DistributionPlottingSettings;
chartSettings: FunctionChartSettings;
height: number;
environment: environment;
};
export const locationAsString = (location: SqValueLocation) =>
location.path.items.join(".");

View File

@ -1,153 +0,0 @@
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 { 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(),
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;
disableLogXSetting?: boolean;
register: UseFormRegister<FormFields>;
}> = ({
withShowEditorSetting = true,
withFunctionSettings = true,
disableLogXSetting,
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"
disabled={disableLogXSetting}
tooltip={
disableLogXSetting
? "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
: undefined
}
/>
<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"
/>
</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>
);
};

View File

@ -1,22 +0,0 @@
import clsx from "clsx";
import React from "react";
type Props = {
onClick: () => void;
children: React.ReactNode;
wide?: boolean; // stretch the button horizontally
};
export const Button: React.FC<Props> = ({ onClick, wide, children }) => {
return (
<button
className={clsx(
"rounded-md py-1.5 px-2 bg-slate-500 text-white text-xs font-semibold flex items-center justify-center space-x-1",
wide && "w-full"
)}
onClick={onClick}
>
{children}
</button>
);
};

View File

@ -1,37 +0,0 @@
import clsx from "clsx";
import React from "react";
import { Path, UseFormRegister, FieldValues } from "react-hook-form";
export function Checkbox<T extends FieldValues>({
name,
label,
register,
disabled,
tooltip,
}: {
name: Path<T>;
label: string;
register: UseFormRegister<T>;
disabled?: boolean;
tooltip?: string;
}) {
return (
<label className="flex items-center" title={tooltip}>
<input
type="checkbox"
disabled={disabled}
{...register(name)}
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
<div
className={clsx(
"ml-3 text-sm font-medium",
disabled ? "text-gray-400" : "text-gray-700"
)}
>
{label}
</div>
</label>
);
}

View File

@ -1,13 +0,0 @@
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>
);

View File

@ -1,25 +0,0 @@
import React from "react";
import { Path, UseFormRegister, FieldValues } from "react-hook-form";
export function InputItem<T extends FieldValues>({
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>
);
}

View File

@ -1,184 +0,0 @@
import { motion } from "framer-motion";
import React, { useContext } from "react";
import * as ReactDOM from "react-dom";
import { XIcon } from "@heroicons/react/solid";
import clsx from "clsx";
import { useWindowScroll, useWindowSize } from "react-use";
type ModalContextShape = {
close: () => void;
};
const ModalContext = React.createContext<ModalContextShape>({
close: () => undefined,
});
const Overlay: React.FC = () => {
const { close } = useContext(ModalContext);
return (
<motion.div
className="absolute inset-0 -z-10 bg-black"
initial={{ opacity: 0 }}
animate={{ opacity: 0.1 }}
onClick={close}
/>
);
};
const ModalHeader: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const { close } = useContext(ModalContext);
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;
container?: HTMLElement;
}> = ({ children, container }) => {
// This component works in two possible modes:
// 1. container mode - the modal is rendered inside a container element
// 2. centered mode - the modal is rendered in the middle of the screen
// The mode is determined by the presence of the `container` prop and by whether the available space is large enough to fit the modal.
// Necessary for container mode - need to reposition the modal on scroll and resize events.
useWindowSize();
useWindowScroll();
let position:
| {
left: number;
top: number;
maxWidth: number;
maxHeight: number;
transform: string;
}
| undefined;
// If available space in `visibleRect` is smaller than these, fallback to positioning in the middle of the screen.
const minWidth = 384;
const minHeight = 300;
const offset = 8;
const naturalWidth = 576; // maximum possible width; modal tries to take this much space, but can be smaller
if (container) {
const { clientWidth: screenWidth, clientHeight: screenHeight } =
document.documentElement;
const rect = container?.getBoundingClientRect();
const visibleRect = {
left: Math.max(rect.left, 0),
right: Math.min(rect.right, screenWidth),
top: Math.max(rect.top, 0),
bottom: Math.min(rect.bottom, screenHeight),
};
const maxWidth = visibleRect.right - visibleRect.left - 2 * offset;
const maxHeight = visibleRect.bottom - visibleRect.top - 2 * offset;
const center = {
left: visibleRect.left + (visibleRect.right - visibleRect.left) / 2,
top: visibleRect.top + (visibleRect.bottom - visibleRect.top) / 2,
};
position = {
left: center.left,
top: center.top,
transform: "translate(-50%, -50%)",
maxWidth,
maxHeight,
};
if (maxWidth < minWidth || maxHeight < minHeight) {
position = undefined; // modal is hard to fit in the container, fallback to positioning it in the middle of the screen
}
}
return (
<div
className={clsx(
"bg-white rounded-md shadow-toast flex flex-col overflow-auto border",
position ? "fixed" : null
)}
style={{
width: naturalWidth,
...(position ?? {
maxHeight: "calc(100% - 20px)",
maxWidth: "calc(100% - 20px)",
width: naturalWidth,
}),
}}
>
{children}
</div>
);
};
type ModalType = React.FC<{
children: React.ReactNode;
container?: HTMLElement; // if specified, modal will be positioned over the visible part of the container, if it's not too small
close: () => void;
}> & {
Body: typeof ModalBody;
Footer: typeof ModalFooter;
Header: typeof ModalHeader;
};
export const Modal: ModalType = ({ children, container, close }) => {
const [el] = React.useState(() => document.createElement("div"));
React.useEffect(() => {
document.body.appendChild(el);
return () => {
document.body.removeChild(el);
};
}, [el]);
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
close();
}
};
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [close]);
const modal = (
<ModalContext.Provider value={{ close }}>
<div className="squiggle">
<div className="fixed inset-0 z-40 flex justify-center items-center">
<Overlay />
<ModalWindow container={container}>{children}</ModalWindow>
</div>
</div>
</ModalContext.Provider>
);
return ReactDOM.createPortal(modal, container || el);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
Modal.Header = ModalHeader;

View File

@ -1,60 +0,0 @@
import React, { Fragment } from "react";
import { Tab } from "@headlessui/react";
import clsx from "clsx";
type StyledTabProps = {
name: string;
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
};
type StyledTabType = React.FC<StyledTabProps> & {
List: React.FC<{ children: React.ReactNode }>;
Group: typeof Tab.Group;
Panels: typeof Tab.Panels;
Panel: typeof Tab.Panel;
};
export const StyledTab: StyledTabType = ({ name, icon: Icon }) => {
return (
<Tab as={Fragment}>
{({ selected }) => (
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
<span
className={clsx(
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5"
)}
>
<Icon
className={clsx(
"-ml-0.5 mr-2 h-4 w-4",
selected
? "text-slate-500"
: "text-gray-400 group-hover:text-gray-900"
)}
/>
<span
className={clsx(
selected
? "text-gray-900"
: "text-gray-600 group-hover:text-gray-900"
)}
>
{name}
</span>
</span>
</button>
)}
</Tab>
);
};
StyledTab.List = ({ children }) => (
<Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
{children}
</Tab.List>
);
StyledTab.Group = Tab.Group;
StyledTab.Panels = Tab.Panels;
StyledTab.Panel = Tab.Panel;

View File

@ -1,5 +0,0 @@
import React from "react";
export const Text: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);

View File

@ -1,47 +0,0 @@
import { RefreshIcon } from "@heroicons/react/solid";
import clsx from "clsx";
import React from "react";
type IconType = (props: React.ComponentProps<"svg">) => JSX.Element;
type Props = {
status: boolean;
onChange: (status: boolean) => void;
texts: [string, string];
icons: [IconType, IconType];
spinIcon?: boolean;
};
export const Toggle: React.FC<Props> = ({
status,
onChange,
texts: [onText, offText],
icons: [OnIcon, OffIcon],
spinIcon,
}) => {
const CurrentIcon = status ? OnIcon : OffIcon;
return (
<button
className={clsx(
"rounded-md py-0.5 bg-slate-500 text-white text-xs font-semibold flex items-center space-x-1",
status ? "bg-slate-500" : "bg-gray-400",
status ? "pl-1 pr-3" : "pl-3 pr-1",
!status && "flex-row-reverse space-x-reverse"
)}
onClick={() => onChange(!status)}
>
<div className="relative w-6 h-6" key={String(spinIcon)}>
<CurrentIcon
className={clsx(
"w-6 h-6 absolute opacity-100",
spinIcon && "animate-hide"
)}
/>
{spinIcon && (
<RefreshIcon className="w-6 h-6 absolute opacity-0 animate-appear-and-spin" />
)}
</div>
<span>{status ? onText : offText}</span>
</button>
);
};

View File

@ -1,64 +0,0 @@
import React, { cloneElement, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import {
flip,
shift,
useDismiss,
useFloating,
useHover,
useInteractions,
useRole,
} from "@floating-ui/react-dom-interactions";
interface Props {
text: string;
children: JSX.Element;
}
export const Tooltip: React.FC<Props> = ({ text, children }) => {
const [isOpen, setIsOpen] = useState(false);
const { x, y, reference, floating, strategy, context } = useFloating({
placement: "top",
open: isOpen,
onOpenChange: setIsOpen,
middleware: [shift(), flip()],
});
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context),
useRole(context, { role: "tooltip" }),
useDismiss(context),
]);
return (
<>
{cloneElement(
children,
getReferenceProps({ ref: reference, ...children.props })
)}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
{...getFloatingProps({
ref: floating,
className:
"text-xs p-2 border border-gray-300 rounded bg-white z-10",
style: {
position: strategy,
top: y ?? 0,
left: x ?? 0,
},
})}
>
<div className="font-mono whitespace-pre">{text}</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};

View File

@ -1,5 +1,6 @@
export { SqProject } from "@quri/squiggle-lang/";
export { SquiggleChart } from "./components/SquiggleChart"; export { SquiggleChart } from "./components/SquiggleChart";
export { SquiggleEditor } from "./components/SquiggleEditor"; export { SquiggleEditor, renderSquiggleEditorToDom } from "./components/SquiggleEditor";
export { SquigglePlayground } from "./components/SquigglePlayground"; import SquigglePlayground, {
export { SquiggleContainer } from "./components/SquiggleContainer"; renderSquigglePlaygroundToDom,
} from "./components/SquigglePlayground";
export { SquigglePlayground, renderSquigglePlaygroundToDom };

View File

@ -1,398 +0,0 @@
import { VisualizationSpec } from "react-vega";
import type { LogScale, LinearScale, PowScale, TimeScale } from "vega";
export type DistributionChartSpecOptions = {
/** Set the x scale to be logarithmic by deault */
logX: boolean;
/** Set the y scale to be exponential by deault */
expY: boolean;
/** The minimum x coordinate shown on the chart */
minX?: number;
/** The maximum x coordinate shown on the chart */
maxX?: number;
/** The title of the chart */
title?: string;
/** The formatting of the ticks */
format?: string;
/** Whether the x-axis should be dates or numbers */
xAxisType?: "number" | "dateTime";
};
/** X Scales */
export const linearXScale: LinearScale = {
name: "xscale",
clamp: true,
type: "linear",
range: "width",
zero: false,
nice: false,
};
export const logXScale: LogScale = {
name: "xscale",
type: "log",
range: "width",
zero: false,
base: 10,
nice: false,
clamp: true,
};
export const timeXScale: TimeScale = {
name: "xscale",
clamp: true,
type: "time",
range: "width",
nice: false,
};
/** Y Scales */
export const linearYScale: LinearScale = {
name: "yscale",
type: "linear",
range: "height",
zero: true,
};
export const expYScale: PowScale = {
name: "yscale",
type: "pow",
exponent: 0.1,
range: "height",
zero: true,
nice: false,
};
export const defaultTickFormat = ".9~s";
export const timeTickFormat = "%b %d, %Y %H:%M";
const width = 500;
export function buildVegaSpec(
specOptions: DistributionChartSpecOptions & { maxY: number }
): VisualizationSpec {
const {
title,
minX,
maxX,
logX,
expY,
xAxisType = "number",
maxY,
} = specOptions;
const dateTime = xAxisType === "dateTime";
// some fallbacks
const format = specOptions?.format
? specOptions.format
: dateTime
? timeTickFormat
: defaultTickFormat;
let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale;
xScale = {
...xScale,
domain: [minX ?? 0, maxX ?? 1],
domainMin: minX,
domainMax: maxX,
};
let yScale = expY ? expYScale : linearYScale;
yScale = { ...yScale, domain: [0, maxY ?? 1], domainMin: 0, domainMax: maxY };
const spec: VisualizationSpec = {
$schema: "https://vega.github.io/schema/vega/v5.json",
description: "Squiggle plot chart",
width: width,
height: 100,
padding: 5,
data: [{ name: "data" }, { name: "domain" }, { name: "samples" }],
signals: [
{
name: "hover",
value: null,
on: [
{ events: "mouseover", update: "datum" },
{ events: "mouseout", update: "null" },
],
},
{
name: "position",
value: "[0, 0]",
on: [
{ events: "mousemove", update: "xy() " },
{ events: "mouseout", update: "null" },
],
},
{
name: "position_scaled",
value: null,
update: "isArray(position) ? invert('xscale', position[0]) : ''",
},
],
scales: [
xScale,
yScale,
{
name: "color",
type: "ordinal",
domain: {
data: "data",
field: "name",
},
range: { scheme: "blues" },
},
],
axes: [
{
orient: "bottom",
scale: "xscale",
labelColor: "#727d93",
tickColor: "#fff",
tickOpacity: 0.0,
domainColor: "#fff",
domainOpacity: 0.0,
format: format,
tickCount: dateTime ? 3 : 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: "continuous_facet",
},
encode: {
update: {
interpolate: { value: "linear" },
x: {
scale: "xscale",
field: "x",
},
y: {
scale: "yscale",
field: "y",
},
fill: {
scale: "color",
field: { parent: "name" },
},
y2: {
scale: "yscale",
value: 0,
},
fillOpacity: {
value: 1,
},
},
},
},
],
},
{
name: "discrete_distribution",
type: "group",
from: {
facet: {
name: "discrete_facet",
data: "distribution_facet",
field: "discrete",
},
},
marks: [
{
type: "rect",
from: {
data: "discrete_facet",
},
encode: {
enter: {
width: {
value: 1,
},
},
update: {
x: {
scale: "xscale",
field: "x",
},
y: {
scale: "yscale",
field: "y",
},
y2: {
scale: "yscale",
value: 0,
},
fill: {
scale: "color",
field: { parent: "name" },
},
},
},
},
{
type: "symbol",
from: {
data: "discrete_facet",
},
encode: {
enter: {
shape: {
value: "circle",
},
size: [{ value: 100 }],
tooltip: {
signal: dateTime
? "{ probability: datum.y, value: datetime(datum.x) }"
: "{ probability: datum.y, value: datum.x }",
},
},
update: {
x: {
scale: "xscale",
field: "x",
offset: 0.5, // if this is not included, the circles are slightly left of center.
},
y: {
scale: "yscale",
field: "y",
},
fill: {
scale: "color",
field: { parent: "name" },
},
},
},
},
],
},
],
},
{
name: "sampleset",
type: "rect",
from: { data: "samples" },
encode: {
enter: {
x: { scale: "xscale", field: "data" },
width: { value: 0.1 },
y: { value: 25, offset: { signal: "height" } },
height: { value: 5 },
},
},
},
{
type: "text",
name: "announcer",
interactive: false,
encode: {
enter: {
x: { signal: String(width), offset: 1 }, // vega would prefer its internal ` "width" ` variable, but that breaks the squiggle playground. Just setting it to the same var as used elsewhere in the spec achieves the same result.
fill: { value: "black" },
fontSize: { value: 20 },
align: { value: "right" },
},
update: {
text: {
signal: dateTime
? "position_scaled ? utcyear(position_scaled) + '-' + utcmonth(position_scaled) + '-' + utcdate(position_scaled) + 'T' + utchours(position_scaled)+':' +utcminutes(position_scaled) : ''"
: "position_scaled ? format(position_scaled, ',.4r') : ''",
},
},
},
},
{
type: "rule",
interactive: false,
encode: {
enter: {
x: { value: 0 },
y: { scale: "yscale", value: 0 },
y2: {
signal: "height",
offset: 2,
},
strokeDash: { value: [5, 5] },
},
update: {
x: {
signal:
"position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null",
},
opacity: {
signal:
"position ? position[0] < 0 ? 0 : position[0] > width ? 0 : 1 : 0",
},
},
},
},
],
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;
}

View File

@ -1,5 +0,0 @@
import { SqShape } from "@quri/squiggle-lang";
export const hasMassBelowZero = (shape: SqShape) =>
shape.continuous.some((x) => x.x <= 0) ||
shape.discrete.some((x) => x.x <= 0);

View File

@ -1,3 +0,0 @@
export { useMaybeControlledValue } from "./useMaybeControlledValue";
export { useSquiggle } from "./useSquiggle";
export { useRunnerState } from "./useRunnerState";

View File

@ -1,22 +0,0 @@
import { useState } from "react";
type ControlledValueArgs<T> = {
value?: T;
defaultValue: T;
onChange?: (x: T) => void;
};
export function useMaybeControlledValue<T>(
args: ControlledValueArgs<T>
): [T, (x: T) => void] {
let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
let value = args.value ?? uncontrolledValue;
let onChange = (newValue: T) => {
if (args.value === undefined) {
// uncontrolled mode
setUncontrolledValue(newValue);
}
args.onChange?.(newValue);
};
return [value, onChange];
}

View File

@ -1,100 +0,0 @@
import { useLayoutEffect, useReducer } from "react";
type State = {
autorunMode: boolean;
renderedCode: string;
// "prepared" is for rendering a spinner; "run" for executing squiggle code; then it gets back to "none" on the next render
runningState: "none" | "prepared" | "run";
executionId: number;
};
const buildInitialState = (code: string): State => ({
autorunMode: true,
renderedCode: "",
runningState: "none",
executionId: 1,
});
type Action =
| {
type: "SET_AUTORUN_MODE";
value: boolean;
code: string;
}
| {
type: "PREPARE_RUN";
}
| {
type: "RUN";
code: string;
}
| {
type: "STOP_RUN";
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_AUTORUN_MODE":
return {
...state,
autorunMode: action.value,
};
case "PREPARE_RUN":
return {
...state,
runningState: "prepared",
};
case "RUN":
return {
...state,
runningState: "run",
renderedCode: action.code,
executionId: state.executionId + 1,
};
case "STOP_RUN":
return {
...state,
runningState: "none",
};
}
};
export const useRunnerState = (code: string) => {
const [state, dispatch] = useReducer(reducer, buildInitialState(code));
useLayoutEffect(() => {
if (state.runningState === "prepared") {
// this is necessary for async playground loading - otherwise it executes the code synchronously on the initial load
// (it's surprising that this is necessary, but empirically it _is_ necessary, both with `useEffect` and `useLayoutEffect`)
setTimeout(() => {
dispatch({ type: "RUN", code });
}, 0);
} else if (state.runningState === "run") {
dispatch({ type: "STOP_RUN" });
}
}, [state.runningState, code]);
const run = () => {
// The rest will be handled by dispatches above on following renders, but we need to update the spinner first.
dispatch({ type: "PREPARE_RUN" });
};
if (
state.autorunMode &&
state.renderedCode !== code &&
state.runningState === "none"
) {
run();
}
return {
run,
autorunMode: state.autorunMode,
renderedCode: state.renderedCode,
isRunning: state.runningState !== "none",
executionId: state.executionId,
setAutorunMode: (newValue: boolean) => {
dispatch({ type: "SET_AUTORUN_MODE", value: newValue, code });
},
};
};

View File

@ -1,97 +0,0 @@
import {
result,
SqError,
SqProject,
SqRecord,
SqValue,
environment,
} from "@quri/squiggle-lang";
import { useEffect, useMemo } from "react";
import { JsImports, jsImportsToSquiggleCode } from "../jsImports";
import * as uuid from "uuid";
type SquiggleArgs = {
environment?: environment;
code: string;
executionId?: number;
jsImports?: JsImports;
project?: SqProject;
continues?: string[];
onChange?: (expr: SqValue | undefined, sourceName: string) => void;
};
export type ResultAndBindings = {
result: result<SqValue, SqError>;
bindings: SqRecord;
};
const importSourceName = (sourceName: string) => "imports-" + sourceName;
const defaultContinues = [];
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const project = useMemo(() => {
if (args.project) {
return args.project;
} else {
const p = SqProject.create();
if (args.environment) {
p.setEnvironment(args.environment);
}
return p;
}
}, [args.project, args.environment]);
const sourceName = useMemo(() => uuid.v4(), []);
const env = project.getEnvironment();
const continues = args.continues || defaultContinues;
const result = useMemo(
() => {
project.setSource(sourceName, args.code);
let fullContinues = continues;
if (args.jsImports && Object.keys(args.jsImports).length) {
const importsSource = jsImportsToSquiggleCode(args.jsImports);
project.setSource(importSourceName(sourceName), importsSource);
fullContinues = continues.concat(importSourceName(sourceName));
}
project.setContinues(sourceName, fullContinues);
project.run(sourceName);
const result = project.getResult(sourceName);
const bindings = project.getBindings(sourceName);
return { result, bindings };
},
// This complains about executionId not being used inside the function body.
// This is on purpose, as executionId simply allows you to run the squiggle
// code again
// eslint-disable-next-line react-hooks/exhaustive-deps
[
args.code,
args.jsImports,
args.executionId,
sourceName,
continues,
project,
env,
]
);
const { onChange } = args;
useEffect(() => {
onChange?.(
result.result.tag === "Ok" ? result.result.value : undefined,
sourceName
);
}, [result, onChange, sourceName]);
useEffect(() => {
return () => {
project.removeSource(sourceName);
if (project.getSource(importSourceName(sourceName)))
project.removeSource(importSourceName(sourceName));
};
}, [project, sourceName]);
return result;
};

View File

@ -1,51 +0,0 @@
type JsImportsValue =
| number
| string
| JsImportsValue[]
| {
[k: string]: JsImportsValue;
};
export type JsImports = {
[k: string]: JsImportsValue;
};
const quote = (arg: string) => `"${arg.replace(new RegExp('"', "g"), '\\"')}"`;
const jsImportsValueToSquiggleCode = (v: JsImportsValue): string => {
if (typeof v === "number") {
return String(v);
} else if (typeof v === "string") {
return quote(v);
} else if (v instanceof Array) {
return "[" + v.map((x) => jsImportsValueToSquiggleCode(x)) + "]";
} else {
if (Object.keys(v).length) {
return (
"{" +
Object.entries(v)
.map(([k, v]) => `${quote(k)}:${jsImportsValueToSquiggleCode(v)},`)
.join("") +
"}"
);
} else {
return "0"; // squiggle doesn't support empty `{}`
}
}
};
export const jsImportsToSquiggleCode = (v: JsImports) => {
const validId = new RegExp("[a-zA-Z][[a-zA-Z0-9]*");
let result = Object.entries(v)
.map(([k, v]) => {
if (!k.match(validId)) {
return ""; // skipping without warnings; can be improved
}
return `$${k} = ${jsImportsValueToSquiggleCode(v)}\n`;
})
.join("");
if (!result) {
result = "$__no_valid_imports__ = 1"; // without this generated squiggle code can be invalid
}
return result;
};

View File

@ -1,83 +0,0 @@
import * as yup from "yup";
import {
SqValue,
SqValueTag,
SqDistribution,
result,
SqRecord,
} from "@quri/squiggle-lang";
export type LabeledDistribution = {
name: string;
distribution: SqDistribution;
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()
.noUnknown()
.strict()
.shape({
distributions: yup
.array()
.required()
.of(
yup.object().required().shape({
name: yup.string().required(),
distribution: yup.mixed().required(),
})
),
});
type JsonObject =
| string
| { [key: string]: JsonObject }
| JsonObject[]
| SqDistribution;
function toJson(val: SqValue): JsonObject {
if (val.tag === SqValueTag.String) {
return val.value;
} else if (val.tag === SqValueTag.Record) {
return toJsonRecord(val.value);
} else if (val.tag === SqValueTag.Array) {
return val.value.getValues().map(toJson);
} else if (val.tag === SqValueTag.Distribution) {
return val.value;
} else {
throw new Error("Could not parse object of type " + val.tag);
}
}
function toJsonRecord(val: SqRecord): JsonObject {
let recordObject: JsonObject = {};
val.entries().forEach(([key, value]) => (recordObject[key] = toJson(value)));
return recordObject;
}
export function parsePlot(record: SqRecord): result<Plot, string> {
try {
const plotRecord = schema.validateSync(toJsonRecord(record));
if (plotRecord.distributions) {
return ok({ distributions: plotRecord.distributions.map((x) => x) });
} else {
// I have no idea why yup's typings thinks this is possible
return error("no distributions field. Should never get here");
}
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
return error(message);
}
}

View File

@ -1,53 +0,0 @@
import { result, resultMap, SqValueTag } from "@quri/squiggle-lang";
import { ResultAndBindings } from "./hooks/useSquiggle";
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);
}
export function getValueToRender({ result, bindings }: ResultAndBindings) {
return resultMap(result, (value) =>
value.tag === SqValueTag.Void ? bindings.asValue() : value
);
}
export function getErrorLocations(result: ResultAndBindings["result"]) {
if (result.tag === "Error") {
const location = result.value.location();
return location ? [location] : [];
} else {
return [];
}
}

View File

@ -14,10 +14,10 @@ It uses the symbols "K", "M", "B", and "T", to represent thousands, millions, bi
name="Ten Thousand" name="Ten Thousand"
args={{ args={{
number: 10000, number: 10000,
precision: 2, precision: 2
}} }}
> >
{(args) => <NumberShower {...args} />} {args => <NumberShower {...args}/>}
</Story> </Story>
</Canvas> </Canvas>
@ -26,10 +26,10 @@ It uses the symbols "K", "M", "B", and "T", to represent thousands, millions, bi
name="Ten Billion" name="Ten Billion"
args={{ args={{
number: 10000000000, number: 10000000000,
precision: 2, precision: 2
}} }}
> >
{(args) => <NumberShower {...args} />} {args => <NumberShower {...args}/>}
</Story> </Story>
</Canvas> </Canvas>
@ -38,10 +38,10 @@ It uses the symbols "K", "M", "B", and "T", to represent thousands, millions, bi
name="1.2*10^15" name="1.2*10^15"
args={{ args={{
number: 1200000000000000, number: 1200000000000000,
precision: 2, precision: 2
}} }}
> >
{(args) => <NumberShower {...args} />} {args => <NumberShower {...args}/>}
</Story> </Story>
</Canvas> </Canvas>
@ -50,10 +50,10 @@ It uses the symbols "K", "M", "B", and "T", to represent thousands, millions, bi
name="1.35*10^-13" name="1.35*10^-13"
args={{ args={{
number: 0.000000000000135, number: 0.000000000000135,
precision: 2, precision: 2
}} }}
> >
{(args) => <NumberShower {...args} />} {args => <NumberShower {...args}/>}
</Story> </Story>
</Canvas> </Canvas>

View File

@ -0,0 +1 @@
{"version":3,"file":"SquiggleChart.stories.js","sourceRoot":"","sources":["SquiggleChart.stories.tsx"],"names":[],"mappings":";;;AAAA,6BAA8B;AAC9B,iDAA+C;AAG/C,qBAAe;IACb,KAAK,EAAE,uBAAuB;IAC9B,SAAS,EAAE,6BAAa;CACzB,CAAA;AAED,IAAM,QAAQ,GAAG,UAAC,EAAgB;QAAf,cAAc,oBAAA;IAAM,OAAA,oBAAC,6BAAa,IAAC,cAAc,EAAE,cAAc,GAAI;AAAjD,CAAiD,CAAA;AAE3E,QAAA,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxC,eAAO,CAAC,IAAI,GAAG;IACb,cAAc,EAAE,cAAc;CAC/B,CAAC"}

View File

@ -3,12 +3,7 @@ import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
<Meta title="Squiggle/SquiggleChart" component={SquiggleChart} /> <Meta title="Squiggle/SquiggleChart" component={SquiggleChart} />
export const Template = (props) => <SquiggleChart {...props} />; export const Template = SquiggleChart;
/*
We have to hardcode a width here, because otherwise some interaction with
Storybook creates an infinite loop with the internal width
*/
const width = 600;
# Squiggle Chart # Squiggle Chart
@ -23,42 +18,13 @@ could be continuous, discrete or mixed.
## Distributions ## Distributions
### Continuous Distributions (Symbolic) ### Continuous Distributions
<Canvas> <Canvas>
<Story <Story
name="Continuous Symbolic" name="Normal"
args={{ args={{
code: "normal(5,2)", squiggleString: "normal(5,2)",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
### Continuous Distributions (PointSet)
<Canvas>
<Story
name="Continuous Pointset"
args={{
code: "PointSet.fromDist(normal(5,2))",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
### Continuous Distributions (SampleSet)
<Canvas>
<Story
name="Continuous SampleSet"
args={{
code: "SampleSet.fromDist(normal(5,2))",
width,
}} }}
> >
{Template.bind({})} {Template.bind({})}
@ -71,24 +37,7 @@ could be continuous, discrete or mixed.
<Story <Story
name="Discrete" name="Discrete"
args={{ args={{
code: "mx(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])", squiggleString: "mm(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
### Date Distribution
<Canvas>
<Story
name="Date Distribution"
args={{
code: "mx(1661819770311, 1661829770311, 1661839770311)",
width,
xAxisType: "dateTime",
width,
}} }}
> >
{Template.bind({})} {Template.bind({})}
@ -101,35 +50,7 @@ could be continuous, discrete or mixed.
<Story <Story
name="Mixed" name="Mixed"
args={{ args={{
code: "mx(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])", squiggleString: "mm(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])",
width,
}}
>
{Template.bind({})}
</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({})} {Template.bind({})}
@ -145,22 +66,24 @@ to allow large and small numbers being printed cleanly.
<Story <Story
name="Constant" name="Constant"
args={{ args={{
code: "500000000", squiggleString: "500000000",
width,
}} }}
> >
{Template.bind({})} {Template.bind({})}
</Story> </Story>
</Canvas> </Canvas>
## Arrays ## Functions
Full functions can be returned. These plot out the results of distributions between a set of x-coordinates.
The default is show 10 points between 0 and 10.
<Canvas> <Canvas>
<Story <Story
name="Array" name="Function"
args={{ args={{
code: "[normal(5,2), normal(10,1), normal(40,2), 400000]", squiggleString: "f(x) = normal(x^2,(x+.1)^1.8)\nf",
width,
}} }}
> >
{Template.bind({})} {Template.bind({})}
@ -173,78 +96,7 @@ to allow large and small numbers being printed cleanly.
<Story <Story
name="Error" name="Error"
args={{ args={{
code: "f(x) = normal(", squiggleString: "f(x) = normal(",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Booleans
<Canvas>
<Story
name="Boolean"
args={{
code: "3 == 3",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Functions (Distribution Output)
<Canvas>
<Story
name="Function to Distribution"
args={{
code: "foo(t) = normal(t,2)*normal(5,3); foo",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Functions (Number Output)
<Canvas>
<Story
name="Function to Number"
args={{
code: "foo(t) = t^2; foo",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Records
<Canvas>
<Story
name="Record"
args={{
code: "{foo: 35 to 50, bar: [1,2,3]}",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Strings
<Canvas>
<Story
name="String"
args={{
code: '"Lucky day!"',
width,
}} }}
> >
{Template.bind({})} {Template.bind({})}

View File

@ -14,20 +14,7 @@ the distribution.
<Story <Story
name="Normal" name="Normal"
args={{ args={{
defaultCode: "normal(5,2)", initialSquiggleString: "normal(5,2)",
}}
>
{Template.bind({})}
</Story>
</Canvas>
It's also possible to create a controlled version of the same component
<Canvas>
<Story
name="Controlled"
args={{
code: "normal(5,2)",
}} }}
> >
{Template.bind({})} {Template.bind({})}
@ -40,7 +27,7 @@ You can also name variables like so:
<Story <Story
name="Variables" name="Variables"
args={{ args={{
defaultCode: "x = 2\nnormal(x,2)", initialSquiggleString: "x = 2\nnormal(x,2)",
}} }}
> >
{Template.bind({})} {Template.bind({})}

View File

@ -1,4 +1,4 @@
import { SquigglePlayground } from "../components/SquigglePlayground"; import SquigglePlayground from "../components/SquigglePlayground";
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
<Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} /> <Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} />
@ -14,21 +14,7 @@ including sampling settings, in squiggle.
<Story <Story
name="Normal" name="Normal"
args={{ args={{
defaultCode: "normal(5,2)", initialSquiggleString: "normal(5,2)",
height: 800,
}}
>
{Template.bind({})}
</Story>
</Canvas>
<Canvas>
<Story
name="With share button"
args={{
defaultCode: "normal(5,2)",
height: 800,
showShareButton: true,
}} }}
> >
{Template.bind({})} {Template.bind({})}

View File

@ -1,396 +0,0 @@
.squiggle {
/*
This file contains:
1) Base Tailwind preflight styles
2) Base https://github.com/tailwindlabs/tailwindcss-forms styles
(Both are wrapped in .squiggle)
*/
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
*/
/* html { */
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-moz-tab-size: 4; /* 3 */
tab-size: 4; /* 3 */
font-family: theme(
"fontFamily.sans",
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
"Noto Sans",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji"
); /* 4 */
/* } */
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
/* body { */
margin: 0; /* 1 */
line-height: inherit; /* 2 */
/* } */
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: theme("borderColor.DEFAULT", currentColor); /* 2 */
}
::before,
::after {
--tw-content: "";
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: theme(
"fontFamily.mono",
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace
); /* 1 */
font-size: 1em; /* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 1 */
background-color: transparent; /* 2 */
background-image: none; /* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::placeholder,
textarea::placeholder {
opacity: 1; /* 1 */
color: theme("colors.gray.400", #9ca3af); /* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
}

View File

@ -1,130 +0,0 @@
/* Fork of https://github.com/tailwindlabs/tailwindcss-forms styles, see the comment in main.css for details. */
.squiggle {
.form-input,
.form-textarea,
.form-select,
.form-multiselect {
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus,
.form-multiselect:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0
calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
var(--tw-shadow);
border-color: #2563eb;
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #6b7280;
opacity: 1;
}
.form-input::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
.form-input::-webkit-date-and-time-value {
min-height: 1.5em;
}
.form-input::-webkit-datetime-edit,
.form-input::-webkit-datetime-edit-year-field,
.form-input::-webkit-datetime-edit-month-field,
.form-input::-webkit-datetime-edit-day-field,
.form-input::-webkit-datetime-edit-hour-field,
.form-input::-webkit-datetime-edit-minute-field,
.form-input::-webkit-datetime-edit-second-field,
.form-input::-webkit-datetime-edit-millisecond-field,
.form-input::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
.form-checkbox,
.form-radio {
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
.form-checkbox {
border-radius: 0px;
}
.form-checkbox:focus,
.form-radio:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0
calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
var(--tw-shadow);
}
.form-checkbox:checked,
.form-radio:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.form-checkbox:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
.form-checkbox:checked:hover,
.form-checkbox:checked:focus,
.form-radio:checked:hover,
.form-radio:checked:focus {
border-color: transparent;
background-color: currentColor;
}
.form-checkbox:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.form-checkbox:indeterminate:hover,
.form-checkbox:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
}

View File

@ -1,29 +0,0 @@
/*
Fork of tailwind's preflight with `.squiggle` scoping.
*/
@import "./base.css";
/*
Fork of https://github.com/tailwindlabs/tailwindcss-forms styles (with strategy: "class"), but with `.squiggle` scoping.
This is necessary because tailwindcss-forms's css specificity is lower than preflight's specificity (`padding: 0` and so on),
so unscoped forms css with scoped preflight css, and there's no setting for scoping.
*/
@import "./forms.css";
/*
This doesn't include tailwind's original preflight (it's disabled in tailwind.config.js),
but this line is still necessary for proper initialization of `--tw-*` variables.
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Necessary hack because scoped preflight in ./base.css has higher specificity. */
.ace_cursor {
border-left: 2px solid !important;
}
.ace-error-marker {
position: absolute;
border-bottom: 1px solid red;
}

View File

@ -12,8 +12,72 @@
"name": "dis" "name": "dis"
} }
], ],
"signals": [], "signals": [
"scales": [], {
"name": "xscale",
"description": "The transform of the x scale",
"value": false,
"bind": {
"input": "checkbox",
"name": "log x scale"
}
},
{
"name": "yscale",
"description": "The transform of the y scale",
"value": false,
"bind": {
"input": "checkbox",
"name": "log y scale"
}
}
],
"scales": [
{
"name": "xscale",
"type": "pow",
"exponent": {
"signal": "xscale ? 0.1 : 1"
},
"range": "width",
"zero": false,
"nice": false,
"domain": {
"fields": [
{
"data": "con",
"field": "x"
},
{
"data": "dis",
"field": "x"
}
]
}
},
{
"name": "yscale",
"type": "pow",
"exponent": {
"signal": "yscale ? 0.1 : 1"
},
"range": "height",
"nice": true,
"zero": true,
"domain": {
"fields": [
{
"data": "con",
"field": "y"
},
{
"data": "dis",
"field": "y"
}
]
}
}
],
"axes": [ "axes": [
{ {
"orient": "bottom", "orient": "bottom",
@ -23,7 +87,7 @@
"tickOpacity": 0.0, "tickOpacity": 0.0,
"domainColor": "#fff", "domainColor": "#fff",
"domainOpacity": 0.0, "domainOpacity": 0.0,
"format": ".9~s", "format": "~s",
"tickCount": 10 "tickCount": 10
} }
], ],
@ -33,7 +97,6 @@
"from": { "from": {
"data": "con" "data": "con"
}, },
"interpolate": "linear",
"encode": { "encode": {
"update": { "update": {
"x": { "x": {
@ -49,10 +112,10 @@
"value": 0 "value": 0
}, },
"fill": { "fill": {
"value": "#739ECC" "signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: '#4C78A8'}] }"
}, },
"interpolate": { "interpolate": {
"value": "linear" "value": "monotone"
}, },
"fillOpacity": { "fillOpacity": {
"value": 1 "value": 1
@ -67,6 +130,10 @@
}, },
"encode": { "encode": {
"enter": { "enter": {
"y2": {
"scale": "yscale",
"value": 0
},
"width": { "width": {
"value": 1 "value": 1
} }
@ -79,13 +146,6 @@
"y": { "y": {
"scale": "yscale", "scale": "yscale",
"field": "y" "field": "y"
},
"y2": {
"scale": "yscale",
"value": 0
},
"fill": {
"value": "#2f65a7"
} }
} }
} }
@ -100,7 +160,7 @@
"shape": { "shape": {
"value": "circle" "value": "circle"
}, },
"size": [{ "value": 100 }], "size": [{"value": 30}],
"tooltip": { "tooltip": {
"signal": "datum.y" "signal": "datum.y"
} }

View File

@ -1,91 +0,0 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 500,
"height": 200,
"padding": 5,
"data": [
{
"name": "facet",
"values": [],
"format": {
"type": "json",
"parse": {
"timestamp": "date"
}
}
}
],
"scales": [
{
"name": "x",
"type": "linear",
"nice": true,
"zero": false,
"domain": {
"data": "facet",
"field": "x"
},
"range": "width"
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true,
"zero": false,
"domain": {
"data": "facet",
"field": "y"
}
}
],
"signals": [
{
"name": "mousemove",
"on": [{ "events": "mousemove", "update": "invert('x', x())" }]
},
{
"name": "mouseout",
"on": [{ "events": "mouseout", "update": "invert('x', x())" }]
}
],
"axes": [
{
"orient": "bottom",
"scale": "x",
"grid": false,
"labelColor": "#727d93",
"tickColor": "#fff",
"tickOpacity": 0.0,
"domainColor": "#727d93",
"domainOpacity": 0.1,
"format": ".9~s",
"tickCount": 5
},
{
"orient": "left",
"scale": "y",
"grid": false,
"labelColor": "#727d93",
"tickColor": "#fff",
"tickOpacity": 0.0,
"domainColor": "#727d93",
"domainOpacity": 0.1,
"format": ".9~s",
"tickCount": 5
}
],
"marks": [
{
"type": "line",
"from": { "data": "facet" },
"encode": {
"enter": {
"x": { "scale": "x", "field": "x" },
"y": { "scale": "y", "field": "y" },
"strokeWidth": { "value": 2 }
}
}
}
]
}

View File

@ -75,7 +75,6 @@
"name": "xscale", "name": "xscale",
"type": "linear", "type": "linear",
"nice": true, "nice": true,
"zero": false,
"domain": { "domain": {
"data": "facet", "data": "facet",
"field": "x" "field": "x"
@ -87,21 +86,21 @@
"type": "linear", "type": "linear",
"range": "height", "range": "height",
"nice": true, "nice": true,
"zero": false, "zero": true,
"domain": { "domain": {
"data": "facet", "data": "facet",
"fields": ["p1", "p99"] "field": "p99"
} }
} }
], ],
"signals": [ "signals": [
{ {
"name": "mousemove", "name": "mousemove",
"on": [{ "events": "mousemove", "update": "invert('xscale', x())" }] "on": [{"events": "mousemove", "update": "invert('xscale', x())"}]
}, },
{ {
"name": "mouseout", "name": "mouseout",
"on": [{ "events": "mouseout", "update": "invert('xscale', x())" }] "on": [{"events": "mouseout", "update": "invert('xscale', x())"}]
} }
], ],
"axes": [ "axes": [
@ -114,14 +113,12 @@
"tickOpacity": 0.0, "tickOpacity": 0.0,
"domainColor": "#727d93", "domainColor": "#727d93",
"domainOpacity": 0.1, "domainOpacity": 0.1,
"format": ".9~s",
"tickCount": 5 "tickCount": 5
}, },
{ {
"orient": "left", "orient": "left",
"scale": "yscale", "scale": "yscale",
"grid": false, "grid": false,
"format": ".9~s",
"labelColor": "#727d93", "labelColor": "#727d93",
"tickColor": "#fff", "tickColor": "#fff",
"tickOpacity": 0.0, "tickOpacity": 0.0,
@ -135,7 +132,7 @@
"type": "rule", "type": "rule",
"encode": { "encode": {
"update": { "update": {
"xscale": { "scale": "xscale", "signal": "mousemove" } "xscale": {"scale": "xscale", "signal": "mousemove"}
} }
} }
}, },

View File

@ -1,31 +0,0 @@
module.exports = {
content: ["./src/**/*.{html,tsx,ts,js,jsx}"],
corePlugins: {
preflight: false,
},
important: ".squiggle",
theme: {
extend: {
animation: {
"appear-and-spin":
"spin 1s linear infinite, squiggle-appear 0.2s forwards",
"semi-appear": "squiggle-semi-appear 0.2s forwards",
hide: "squiggle-hide 0.2s forwards",
},
keyframes: {
"squiggle-appear": {
from: { opacity: 0 },
to: { opacity: 1 },
},
"squiggle-semi-appear": {
from: { opacity: 0 },
to: { opacity: 0.5 },
},
"squiggle-hide": {
from: { opacity: 1 },
to: { opacity: 0 },
},
},
},
},
};

Some files were not shown because too many files have changed in this diff Show More