34
README.md
|
@ -2,13 +2,31 @@
|
||||||
|
|
||||||
This is an experiment DSL/language for making probabilistic estimates.
|
This is an experiment DSL/language for making probabilistic estimates.
|
||||||
|
|
||||||
## DistPlus
|
This monorepo has several packages that can be used for various purposes. All
|
||||||
We have a custom library called DistPlus to handle distributions with additional metadata. This helps handle mixed distributions (continuous + discrete), a cache for a cdf, possible unit types (specific times are supported), and limited domains.
|
the packages can be found in `packages`.
|
||||||
|
|
||||||
## Running packages in the monorepo
|
`@squiggle/lang` in `packages/squiggle-lang` contains the core language, particularly
|
||||||
This application uses `lerna` to manage dependencies between packages. To install
|
an interface to parse squiggle expressions and return descriptions of distributions
|
||||||
dependencies of all packages, run:
|
or results.
|
||||||
|
|
||||||
|
`@squiggle/components` in `packages/components` contains React components that
|
||||||
|
can be passed squiggle strings as props, and return a presentation of the result
|
||||||
|
of the calculation.
|
||||||
|
|
||||||
|
`@squiggle/playground` in `packages/playground` contains a website for a playground
|
||||||
|
for squiggle. This website is hosted at `playground.squiggle-language.com`
|
||||||
|
|
||||||
|
`@squiggle/website` in `packages/website` The main descriptive website for squiggle,
|
||||||
|
it is hosted at `squiggle-language.com`.
|
||||||
|
|
||||||
|
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 package the language, and for the playground to work, you will need to package
|
||||||
|
the components library and the playground.
|
||||||
|
|
||||||
|
Scripts are available for you in the root directory to do important activities,
|
||||||
|
such as:
|
||||||
|
|
||||||
|
`yarn build:lang`. Builds and packages the language
|
||||||
|
`yarn storybook:components`. Hosts the component storybook
|
||||||
|
|
||||||
```
|
|
||||||
lerna bootstrap
|
|
||||||
```
|
|
||||||
|
|
|
@ -4,5 +4,11 @@
|
||||||
"lerna": "^4.0.0"
|
"lerna": "^4.0.0"
|
||||||
},
|
},
|
||||||
"name": "squiggle",
|
"name": "squiggle",
|
||||||
|
"scripts": {
|
||||||
|
"build:lang": "cd packages/squiggle-lang && yarn && yarn build && yarn package",
|
||||||
|
"storybook:components": "cd packages/components && yarn && yarn storybook",
|
||||||
|
"build-storybook:components": "cd packages/components && yarn && yarn build-storybook",
|
||||||
|
"build:components": "cd packages/components && yarn && yarn package"
|
||||||
|
},
|
||||||
"workspaces": ["packages/*"]
|
"workspaces": ["packages/*"]
|
||||||
}
|
}
|
||||||
|
|
25
packages/components/.gitignore
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
storybook-static
|
||||||
|
dist
|
26
packages/components/.storybook/main.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
//const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/* webpackFinal: async (config) => {
|
||||||
|
config.resolve.plugins = [
|
||||||
|
...(config.resolve.plugins || []),
|
||||||
|
new TsconfigPathsPlugin({
|
||||||
|
extensions: config.resolve.extensions,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return config;
|
||||||
|
},*/
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.stories.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@storybook/preset-create-react-app"
|
||||||
|
],
|
||||||
|
"framework": "@storybook/react",
|
||||||
|
"core": {
|
||||||
|
"builder": "webpack5"
|
||||||
|
}
|
||||||
|
}
|
9
packages/components/.storybook/preview.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const parameters = {
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
6
packages/components/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# 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/).
|
||||||
|
|
||||||
|
To run the storybook, run `yarn` then `yarn storybook`.
|
57037
packages/components/package-lock.json
generated
Normal file
73
packages/components/package.json
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"name": "@squiggle/components",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@squiggle/lang": "^0.1.9",
|
||||||
|
"@testing-library/jest-dom": "^5.16.2",
|
||||||
|
"@testing-library/react": "^12.1.2",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.4.0",
|
||||||
|
"@types/lodash": "^4.14.178",
|
||||||
|
"@types/node": "^17.0.16",
|
||||||
|
"@types/react": "^17.0.39",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-scripts": "5.0.0",
|
||||||
|
"react-vega": "^7.4.4",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
|
"vega-embed": "^6.20.6",
|
||||||
|
"web-vitals": "^2.1.4",
|
||||||
|
"webpack-cli": "^4.9.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"storybook": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
|
||||||
|
"build-storybook": "build-storybook -s public",
|
||||||
|
"package": "tsc"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.stories.*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"import/no-anonymous-default-export": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@storybook/addon-actions": "^6.4.18",
|
||||||
|
"@storybook/addon-essentials": "^6.4.18",
|
||||||
|
"@storybook/addon-links": "^6.4.18",
|
||||||
|
"@storybook/builder-webpack5": "^6.4.18",
|
||||||
|
"@storybook/manager-webpack5": "^6.4.18",
|
||||||
|
"@storybook/node-logger": "^6.4.18",
|
||||||
|
"@storybook/preset-create-react-app": "^4.0.0",
|
||||||
|
"@storybook/react": "^6.4.18",
|
||||||
|
"webpack": "^5.68.0"
|
||||||
|
},
|
||||||
|
"main": "dist/lib.js",
|
||||||
|
"types": "dist/lib.d.ts"
|
||||||
|
}
|
BIN
packages/components/public/favicon.ico
Normal file
After Width: | Height: | Size: 13 KiB |
19
packages/components/public/index.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Squiggle components"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<title>Squiggle Components</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
packages/components/public/logo16.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
packages/components/public/logo192.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
packages/components/public/logo32.png
Normal file
After Width: | Height: | Size: 697 B |
BIN
packages/components/public/logo42.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
packages/components/public/logo512.png
Normal file
After Width: | Height: | Size: 31 KiB |
25
packages/components/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
packages/components/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
676
packages/components/public/squiggle.svg
Normal file
After Width: | Height: | Size: 107 KiB |
5
packages/components/shell.nix
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
pkgs.mkShell {
|
||||||
|
name = "squiggle-components";
|
||||||
|
buildInputs = with pkgs; [ nodePackages.yarn nodejs ];
|
||||||
|
}
|
1
packages/components/src/SquiggleChart.js.map
Normal file
344
packages/components/src/SquiggleChart.tsx
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as PropTypes from 'prop-types';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import type { Spec } from 'vega';
|
||||||
|
import { run } from '@squiggle/lang';
|
||||||
|
import type { DistPlus } from '@squiggle/lang';
|
||||||
|
import { createClassFromSpec } from 'react-vega';
|
||||||
|
import * as chartSpecification from './spec-distributions.json'
|
||||||
|
import * as percentilesSpec from './spec-pertentiles.json'
|
||||||
|
|
||||||
|
let SquiggleVegaChart = createClassFromSpec({'spec': chartSpecification as Spec});
|
||||||
|
|
||||||
|
let SquigglePercentilesChart = createClassFromSpec({'spec': percentilesSpec as Spec});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary UI component for user interaction
|
||||||
|
*/
|
||||||
|
export const SquiggleChart = ({ squiggleString }: { squiggleString: string}) => {
|
||||||
|
|
||||||
|
let result = run(squiggleString);
|
||||||
|
console.log(result)
|
||||||
|
if (result.tag === "Ok") {
|
||||||
|
let chartResults = result.value.map(chartResult => {
|
||||||
|
console.log(chartResult)
|
||||||
|
if(chartResult["NAME"] === "Float"){
|
||||||
|
return <MakeNumberShower precision={3} number={chartResult["VAL"]} />;
|
||||||
|
}
|
||||||
|
else if(chartResult["NAME"] === "DistPlus"){
|
||||||
|
let shape = chartResult.VAL.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
|
||||||
|
data={{"con": values}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(chartResult.NAME === "Function"){
|
||||||
|
// We are looking at a function. In this case, we draw a Percentiles chart
|
||||||
|
let data = _.range(0,10,0.1).map((_,i) => {
|
||||||
|
let x = i /10;
|
||||||
|
if(chartResult.NAME=="Function"){
|
||||||
|
let result = chartResult.VAL(x);
|
||||||
|
if(result.tag == "Ok"){
|
||||||
|
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 percentiles = getPercentiles(percentileArray, result.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]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
return <SquigglePercentilesChart data={{"facet": data}} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return <>{chartResults}</>;
|
||||||
|
}
|
||||||
|
else if(result.tag == "Error") {
|
||||||
|
// At this point, we came across an error. What was our error?
|
||||||
|
return (<p>{"Error parsing Squiggle: " + result.value}</p>)
|
||||||
|
|
||||||
|
}
|
||||||
|
return (<p>{"Invalid Response"}</p>)
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPercentiles(percentiles:number[], t : DistPlus) {
|
||||||
|
if(t.pointSetDist.tag == "Discrete") {
|
||||||
|
let total = 0;
|
||||||
|
let maxX = _.max(t.pointSetDist.value.xyShape.xs)
|
||||||
|
let bounds = percentiles.map(_ => maxX);
|
||||||
|
_.zipWith(t.pointSetDist.value.xyShape.xs,t.pointSetDist.value.xyShape.ys, (x,y) => {
|
||||||
|
total += y
|
||||||
|
percentiles.forEach((v, i) => {
|
||||||
|
if(total > v && bounds[i] == maxX){
|
||||||
|
bounds[i] = x
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
else if(t.pointSetDist.tag == "Continuous"){
|
||||||
|
let total = 0;
|
||||||
|
let maxX = _.max(t.pointSetDist.value.xyShape.xs)
|
||||||
|
let totalY = _.sum(t.pointSetDist.value.xyShape.ys)
|
||||||
|
let bounds = percentiles.map(_ => maxX);
|
||||||
|
_.zipWith(t.pointSetDist.value.xyShape.xs,t.pointSetDist.value.xyShape.ys, (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"
|
||||||
|
};
|
||||||
|
|
||||||
|
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 maxX = _.max(sortedPoints.map(x => x.x));
|
||||||
|
let bounds = percentiles.map(_ => maxX);
|
||||||
|
sortedPoints.map((point: labeledPoint) => {
|
||||||
|
if(point.type == "discrete") {
|
||||||
|
total += point.y;
|
||||||
|
}
|
||||||
|
else if (point.type == "continuous") {
|
||||||
|
total += point.y / totalY * totalContinuous;
|
||||||
|
}
|
||||||
|
percentiles.forEach((v,i) => {
|
||||||
|
if(total > v && bounds[i] == maxX){
|
||||||
|
bounds[i] = total;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return total;
|
||||||
|
});
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SquiggleChart.propTypes = {
|
||||||
|
/**
|
||||||
|
* Squiggle String
|
||||||
|
*/
|
||||||
|
squiggleString : PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
SquiggleChart.defaultProps = {
|
||||||
|
squggleString: "normal(5, 2)"
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function MakeNumberShower(props: {number: number, precision :number}){
|
||||||
|
let numberWithPresentation = numberShow(props.number, props.precision);
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{numberWithPresentation.value}
|
||||||
|
{numberWithPresentation.symbol}
|
||||||
|
{numberWithPresentation.power ?
|
||||||
|
<span>
|
||||||
|
{'\u00b710'}
|
||||||
|
<span style={{fontSize: "0.6em", verticalAlign: "super"}}>
|
||||||
|
{numberWithPresentation.power}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
: <></>}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderOfMagnitudeNum = (n:number) => {
|
||||||
|
return Math.pow(10, n);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 105 -> 3
|
||||||
|
const orderOfMagnitude = (n:number) => {
|
||||||
|
return Math.floor(Math.log(n) / Math.LN10 + 0.000000001);
|
||||||
|
};
|
||||||
|
|
||||||
|
function withXSigFigs(number:number, sigFigs:number) {
|
||||||
|
const withPrecision = number.toPrecision(sigFigs);
|
||||||
|
const formatted = Number(withPrecision);
|
||||||
|
return `${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumberShower {
|
||||||
|
number: number
|
||||||
|
precision: number
|
||||||
|
|
||||||
|
constructor(number:number, precision = 2) {
|
||||||
|
this.number = number;
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
const ns = new NumberShower(number, precision);
|
||||||
|
return ns.convert();
|
||||||
|
}
|
1
packages/components/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { SquiggleChart } from './SquiggleChart';
|
122
packages/components/src/spec-distributions.json
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
||||||
|
"description": "A basic area chart example.",
|
||||||
|
"width": 500,
|
||||||
|
"height": 200,
|
||||||
|
"padding": 5,
|
||||||
|
"data": [{"name": "con"}, {"name": "dis"}],
|
||||||
|
|
||||||
|
"signals": [
|
||||||
|
{
|
||||||
|
"name": "mousex",
|
||||||
|
"description": "x position of mouse",
|
||||||
|
"update": "0",
|
||||||
|
"on": [{"events": "mousemove", "update": "1-x()/width"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xscale",
|
||||||
|
"description": "The transform of the x scale",
|
||||||
|
"value": 1.0,
|
||||||
|
"bind": {
|
||||||
|
"input": "range",
|
||||||
|
"min": 0.1,
|
||||||
|
"max": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yscale",
|
||||||
|
"description": "The transform of the y scale",
|
||||||
|
"value": 1.0,
|
||||||
|
"bind": {
|
||||||
|
"input": "range",
|
||||||
|
"min": 0.1,
|
||||||
|
"max": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"scales": [{
|
||||||
|
"name": "xscale",
|
||||||
|
"type": "pow",
|
||||||
|
"exponent": {"signal": "xscale"},
|
||||||
|
"range": "width",
|
||||||
|
"zero": false,
|
||||||
|
"nice": false,
|
||||||
|
"domain": {
|
||||||
|
"fields": [
|
||||||
|
{ "data": "con", "field": "x"},
|
||||||
|
{ "data": "dis", "field": "x"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "yscale",
|
||||||
|
"type": "pow",
|
||||||
|
"exponent": {"signal": "yscale"},
|
||||||
|
"range": "height",
|
||||||
|
"nice": true,
|
||||||
|
"zero": true,
|
||||||
|
"domain": {
|
||||||
|
"fields": [
|
||||||
|
{ "data": "con", "field": "y"},
|
||||||
|
{ "data": "dis", "field": "y"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"axes": [
|
||||||
|
{"orient": "bottom", "scale": "xscale", "tickCount": 20},
|
||||||
|
{"orient": "left", "scale": "yscale"}
|
||||||
|
],
|
||||||
|
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": {"data": "con"},
|
||||||
|
"encode": {
|
||||||
|
"enter": {
|
||||||
|
"tooltip": {"signal": "datum.cdf"}
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"x": {"scale": "xscale", "field": "x"},
|
||||||
|
"y": {"scale": "yscale", "field": "y"},
|
||||||
|
"y2": {"scale": "yscale", "value": 0},
|
||||||
|
"fill": {
|
||||||
|
"signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'blue'}, {offset: 1.0, color: 'blue'} ] }"
|
||||||
|
},
|
||||||
|
"interpolate": {"value": "monotone"},
|
||||||
|
"fillOpacity": {"value": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "rect",
|
||||||
|
"from": {"data": "dis"},
|
||||||
|
"encode": {
|
||||||
|
"enter": {
|
||||||
|
"y2": {"scale": "yscale", "value": 0},
|
||||||
|
"width": {"value": 1}
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"x": {"scale": "xscale", "field": "x"},
|
||||||
|
"y": {"scale": "yscale", "field": "y"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "symbol",
|
||||||
|
"from": {"data": "dis"},
|
||||||
|
"encode": {
|
||||||
|
"enter": {
|
||||||
|
"shape": {"value": "circle"},
|
||||||
|
"width": {"value": 5},
|
||||||
|
"tooltip": {"signal": "datum.y"}
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"x": {"scale": "xscale", "field": "x"},
|
||||||
|
"y": {"scale": "yscale", "field": "y"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
208
packages/components/src/spec-pertentiles.json
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
||||||
|
"width": 500,
|
||||||
|
"height": 400,
|
||||||
|
"padding": 5,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "facet",
|
||||||
|
"values": [],
|
||||||
|
"format": { "type": "json", "parse": { "timestamp": "date" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "table",
|
||||||
|
"source": "facet",
|
||||||
|
"transform": [
|
||||||
|
{
|
||||||
|
"type": "aggregate",
|
||||||
|
"groupby": ["x"],
|
||||||
|
"ops": [
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean",
|
||||||
|
"mean"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
"p1",
|
||||||
|
"p5",
|
||||||
|
"p10",
|
||||||
|
"p20",
|
||||||
|
"p30",
|
||||||
|
"p40",
|
||||||
|
"p50",
|
||||||
|
"p60",
|
||||||
|
"p70",
|
||||||
|
"p80",
|
||||||
|
"p90",
|
||||||
|
"p95",
|
||||||
|
"p99"
|
||||||
|
],
|
||||||
|
"as": [
|
||||||
|
"p1",
|
||||||
|
"p5",
|
||||||
|
"p10",
|
||||||
|
"p20",
|
||||||
|
"p30",
|
||||||
|
"p40",
|
||||||
|
"p50",
|
||||||
|
"p60",
|
||||||
|
"p70",
|
||||||
|
"p80",
|
||||||
|
"p90",
|
||||||
|
"p95",
|
||||||
|
"p99"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scales": [
|
||||||
|
{
|
||||||
|
"name": "xscale",
|
||||||
|
"type": "linear",
|
||||||
|
"nice": true,
|
||||||
|
"domain": { "data": "facet", "field": "x" },
|
||||||
|
"range": "width"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yscale",
|
||||||
|
"type": "linear",
|
||||||
|
"range": "height",
|
||||||
|
"nice": true,
|
||||||
|
"zero": true,
|
||||||
|
"domain": { "data": "facet", "field": "p99" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"axes": [
|
||||||
|
{
|
||||||
|
"orient": "bottom",
|
||||||
|
"scale": "xscale",
|
||||||
|
"grid": false,
|
||||||
|
"tickSize": 2,
|
||||||
|
"encode": {
|
||||||
|
"grid": { "enter": { "stroke": { "value": "#ccc" } } },
|
||||||
|
"ticks": { "enter": { "stroke": { "value": "#ccc" } } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orient": "left",
|
||||||
|
"scale": "yscale",
|
||||||
|
"grid": false,
|
||||||
|
"domain": false,
|
||||||
|
"tickSize": 2,
|
||||||
|
"encode": {
|
||||||
|
"grid": { "enter": { "stroke": { "value": "#ccc" } } },
|
||||||
|
"ticks": { "enter": { "stroke": { "value": "#ccc" } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"enter": { "fill": { "value": "#4C78A8" } },
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p1" },
|
||||||
|
"y2": { "scale": "yscale", "field": "p99" },
|
||||||
|
"opacity": { "value": 0.05 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"enter": { "fill": { "value": "#4C78A8" } },
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p5" },
|
||||||
|
"y2": { "scale": "yscale", "field": "p95" },
|
||||||
|
"opacity": { "value": 0.1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"enter": { "fill": { "value": "#4C78A8" } },
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p10" },
|
||||||
|
"y2": { "scale": "yscale", "field": "p90" },
|
||||||
|
"opacity": { "value": 0.15 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"enter": { "fill": { "value": "#4C78A8" } },
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p20" },
|
||||||
|
"y2": { "scale": "yscale", "field": "p80" },
|
||||||
|
"opacity": { "value": 0.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"enter": { "fill": { "value": "#4C78A8" } },
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p30" },
|
||||||
|
"y2": { "scale": "yscale", "field": "p70" },
|
||||||
|
"opacity": { "value": 0.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"enter": { "fill": { "value": "#4C78A8" } },
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p40" },
|
||||||
|
"y2": { "scale": "yscale", "field": "p60" },
|
||||||
|
"opacity": { "value": 0.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "line",
|
||||||
|
"from": { "data": "table" },
|
||||||
|
"encode": {
|
||||||
|
"update": {
|
||||||
|
"interpolate": { "value": "monotone" },
|
||||||
|
"stroke": { "value": "#4C78A8" },
|
||||||
|
"strokeWidth": { "value": 2 },
|
||||||
|
"opacity": { "value": 0.8 },
|
||||||
|
"x": { "scale": "xscale", "field": "x" },
|
||||||
|
"y": { "scale": "yscale", "field": "p50" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
packages/components/src/stories/Introduction.stories.mdx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Meta } from '@storybook/addon-docs';
|
||||||
|
|
||||||
|
<Meta title="Squiggle/Introduction" />
|
||||||
|
|
||||||
|
This is the component library for Squiggle. All of these components are react
|
||||||
|
components, and can be used in any application that you see fit.
|
||||||
|
|
||||||
|
Currently, the only component that is provided is the SquiggleChart component.
|
||||||
|
This component allows you to render the result of a squiggle expression.
|
|
@ -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"}
|
81
packages/components/src/stories/SquiggleChart.stories.mdx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { SquiggleChart } from '../SquiggleChart'
|
||||||
|
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||||
|
|
||||||
|
<Meta title="Squiggle/SquiggleChart" component={ SquiggleChart } />
|
||||||
|
|
||||||
|
export const Template = ({squiggleString}) => <SquiggleChart squiggleString={squiggleString} />
|
||||||
|
|
||||||
|
# Squiggle Chart
|
||||||
|
|
||||||
|
Squiggle chart evaluates squiggle expressions, and then returns a graph representing
|
||||||
|
the result of a squiggle expression.
|
||||||
|
|
||||||
|
A squiggle expression can have three different types of returns. A distribution,
|
||||||
|
a constant, and a function.
|
||||||
|
|
||||||
|
A distribution means that the result forms a probability distribution. This
|
||||||
|
could be continuous, discrete or mixed.
|
||||||
|
|
||||||
|
## Distributions
|
||||||
|
|
||||||
|
An example of a normal distribution is:
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Normal"
|
||||||
|
args={{
|
||||||
|
squiggleString: "normal(5,2)"
|
||||||
|
}}>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
|
||||||
|
An example of a Discrete distribution is:
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Discrete"
|
||||||
|
args={{
|
||||||
|
squiggleString: "mm(0, 1, [0.5, 0.5])"
|
||||||
|
}}>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
An example of a Mixed distribution is:
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Mixed"
|
||||||
|
args={{
|
||||||
|
squiggleString: "mm(0, 5 to 10, [0.5, 0.5])"
|
||||||
|
}}>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
A constant is a simple number as a result. This has special formatting rules
|
||||||
|
to allow large and small numbers being printed cleanly.
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Constant"
|
||||||
|
args={{
|
||||||
|
squiggleString: "500000 * 5000000"
|
||||||
|
}}>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
Finally, a function can be returned, and this shows how the distribution changes
|
||||||
|
over the axis between x = 0 and 10.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Function"
|
||||||
|
args={{
|
||||||
|
squiggleString: "f(x) = normal(x,x)\nf"
|
||||||
|
}}>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
17
packages/components/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"jsx": "react",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declarationDir": "./dist",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"target": "ES6",
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||||
|
}
|