Moved NumberShower to its own Storybook, and slight cleanup to other code.

This commit is contained in:
Ozzie Gooen 2022-04-06 17:51:24 -04:00
parent 952ce9da11
commit 4d0a522e96
7 changed files with 504 additions and 194 deletions

View File

@ -0,0 +1,98 @@
import * as React from "react";
import _ from "lodash";
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 NumberShowerBuilder {
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 NumberShowerBuilder(number, precision);
return ns.convert();
}
export interface NumberShowerProps {
number: number;
precision?: number
}
export let NumberShower: React.FC<NumberShowerProps> = ({
number,
precision = 2
}: NumberShowerProps) => {
let numberWithPresentation = numberShow(number, precision);
return (
<span>
{numberWithPresentation.value}
{numberWithPresentation.symbol}
{numberWithPresentation.power ? (
<span>
{"\u00b710"}
<span style={{ fontSize: "0.6em", verticalAlign: "super" }}>
{numberWithPresentation.power}
</span>
</span>
) : (
<></>
)}
</span>
);
}

View File

@ -11,6 +11,7 @@ import type {
import { createClassFromSpec } from "react-vega"; import { createClassFromSpec } from "react-vega";
import * as chartSpecification from "./spec-distributions.json"; import * as chartSpecification from "./spec-distributions.json";
import * as percentilesSpec from "./spec-percentiles.json"; import * as percentilesSpec from "./spec-percentiles.json";
import { NumberShower } from "./NumberShower";
let SquiggleVegaChart = createClassFromSpec({ let SquiggleVegaChart = createClassFromSpec({
spec: chartSpecification as Spec, spec: chartSpecification as Spec,
@ -71,7 +72,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
onEnvChange(environment); onEnvChange(environment);
let chartResults = exports.map((chartResult: exportDistribution) => { let chartResults = exports.map((chartResult: exportDistribution) => {
if (chartResult["NAME"] === "Float") { if (chartResult["NAME"] === "Float") {
return <MakeNumberShower precision={3} number={chartResult["VAL"]} />; return <NumberShower precision={3} number={chartResult["VAL"]} />;
} else if (chartResult["NAME"] === "DistPlus") { } else if (chartResult["NAME"] === "DistPlus") {
let shape = chartResult.VAL.pointSetDist; let shape = chartResult.VAL.pointSetDist;
if (shape.tag === "Continuous") { if (shape.tag === "Continuous") {
@ -317,91 +318,3 @@ function getPercentiles(percentiles: number[], t: DistPlus) {
return bounds; return bounds;
} }
} }
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();
}

View File

@ -4,14 +4,25 @@
"width": 500, "width": 500,
"height": 200, "height": 200,
"padding": 5, "padding": 5,
"data": [{ "name": "con" }, { "name": "dis" }], "data": [
{
"name": "con"
},
{
"name": "dis"
}
],
"signals": [ "signals": [
{ {
"name": "mousex", "name": "mousex",
"description": "x position of mouse", "description": "x position of mouse",
"update": "0", "update": "0",
"on": [{ "events": "mousemove", "update": "1-x()/width" }] "on": [
{
"events": "mousemove",
"update": "1-x()/width"
}
]
}, },
{ {
"name": "xscale", "name": "xscale",
@ -32,87 +43,150 @@
} }
} }
], ],
"scales": [ "scales": [
{ {
"name": "xscale", "name": "xscale",
"type": "pow", "type": "pow",
"exponent": { "signal": "xscale ? 0.1 : 1" }, "exponent": {
"signal": "xscale ? 0.1 : 1"
},
"range": "width", "range": "width",
"zero": false, "zero": false,
"nice": false, "nice": false,
"domain": { "domain": {
"fields": [ "fields": [
{ "data": "con", "field": "x" }, {
{ "data": "dis", "field": "x" } "data": "con",
"field": "x"
},
{
"data": "dis",
"field": "x"
}
] ]
} }
}, },
{ {
"name": "yscale", "name": "yscale",
"type": "pow", "type": "pow",
"exponent": { "signal": "yscale ? 0.1 : 1" }, "exponent": {
"signal": "yscale ? 0.1 : 1"
},
"range": "height", "range": "height",
"nice": true, "nice": true,
"zero": true, "zero": true,
"domain": { "domain": {
"fields": [ "fields": [
{ "data": "con", "field": "y" }, {
{ "data": "dis", "field": "y" } "data": "con",
"field": "y"
},
{
"data": "dis",
"field": "y"
}
] ]
} }
} }
], ],
"axes": [
"axes": [{ "orient": "bottom", "scale": "xscale", "tickCount": 20 }], {
"orient": "bottom",
"scale": "xscale",
"tickCount": 20
}
],
"marks": [ "marks": [
{ {
"type": "area", "type": "area",
"from": { "data": "con" }, "from": {
"data": "con"
},
"encode": { "encode": {
"enter": { "enter": {
"tooltip": { "signal": "datum.cdf" } "tooltip": {
"signal": "datum.cdf"
}
}, },
"update": { "update": {
"x": { "scale": "xscale", "field": "x" }, "x": {
"y": { "scale": "yscale", "field": "y" }, "scale": "xscale",
"y2": { "scale": "yscale", "value": 0 }, "field": "x"
},
"y": {
"scale": "yscale",
"field": "y"
},
"y2": {
"scale": "yscale",
"value": 0
},
"fill": { "fill": {
"signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: '#11ac8f'}, {offset: clamp(mousex, 0, 1), color: '#11ac8f'}, {offset: clamp(mousex, 0, 1), color: '#1b6fac'}, {offset: 1.0, color: '#1b6fac'} ] }", "signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: '#11ac8f'}, {offset: clamp(mousex, 0, 1), color: '#11ac8f'}, {offset: clamp(mousex, 0, 1), color: '#1b6fac'}, {offset: 1.0, color: '#1b6fac'} ] }",
"color": "#000" "color": "#000"
}, },
"interpolate": { "value": "monotone" }, "interpolate": {
"fillOpacity": { "value": 1 } "value": "monotone"
},
"fillOpacity": {
"value": 1
}
} }
} }
}, },
{ {
"type": "rect", "type": "rect",
"from": { "data": "dis" }, "from": {
"data": "dis"
},
"encode": { "encode": {
"enter": { "enter": {
"y2": { "scale": "yscale", "value": 0 }, "y2": {
"width": { "value": 1 } "scale": "yscale",
"value": 0
},
"width": {
"value": 1
}
}, },
"update": { "update": {
"x": { "scale": "xscale", "field": "x" }, "x": {
"y": { "scale": "yscale", "field": "y" } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "y"
}
} }
} }
}, },
{ {
"type": "symbol", "type": "symbol",
"from": { "data": "dis" }, "from": {
"data": "dis"
},
"encode": { "encode": {
"enter": { "enter": {
"shape": { "value": "circle" }, "shape": {
"width": { "value": 5 }, "value": "circle"
"tooltip": { "signal": "datum.y" } },
"width": {
"value": 5
},
"tooltip": {
"signal": "datum.y"
}
}, },
"update": { "update": {
"x": { "scale": "xscale", "field": "x" }, "x": {
"y": { "scale": "yscale", "field": "y" } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "y"
}
} }
} }
} }

View File

@ -7,7 +7,12 @@
{ {
"name": "facet", "name": "facet",
"values": [], "values": [],
"format": { "type": "json", "parse": { "timestamp": "date" } } "format": {
"type": "json",
"parse": {
"timestamp": "date"
}
}
}, },
{ {
"name": "table", "name": "table",
@ -15,7 +20,9 @@
"transform": [ "transform": [
{ {
"type": "aggregate", "type": "aggregate",
"groupby": ["x"], "groupby": [
"x"
],
"ops": [ "ops": [
"mean", "mean",
"mean", "mean",
@ -70,7 +77,10 @@
"name": "xscale", "name": "xscale",
"type": "linear", "type": "linear",
"nice": true, "nice": true,
"domain": { "data": "facet", "field": "x" }, "domain": {
"data": "facet",
"field": "x"
},
"range": "width" "range": "width"
}, },
{ {
@ -79,7 +89,10 @@
"range": "height", "range": "height",
"nice": true, "nice": true,
"zero": true, "zero": true,
"domain": { "data": "facet", "field": "p99" } "domain": {
"data": "facet",
"field": "p99"
}
} }
], ],
"axes": [ "axes": [
@ -89,8 +102,20 @@
"grid": false, "grid": false,
"tickSize": 2, "tickSize": 2,
"encode": { "encode": {
"grid": { "enter": { "stroke": { "value": "#ccc" } } }, "grid": {
"ticks": { "enter": { "stroke": { "value": "#ccc" } } } "enter": {
"stroke": {
"value": "#ccc"
}
}
},
"ticks": {
"enter": {
"stroke": {
"value": "#ccc"
}
}
}
} }
}, },
{ {
@ -100,107 +125,249 @@
"domain": false, "domain": false,
"tickSize": 2, "tickSize": 2,
"encode": { "encode": {
"grid": { "enter": { "stroke": { "value": "#ccc" } } }, "grid": {
"ticks": { "enter": { "stroke": { "value": "#ccc" } } } "enter": {
"stroke": {
"value": "#ccc"
}
}
},
"ticks": {
"enter": {
"stroke": {
"value": "#ccc"
}
}
}
} }
} }
], ],
"marks": [ "marks": [
{ {
"type": "area", "type": "area",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"enter": { "fill": { "value": "#4C78A8" } }, "enter": {
"fill": {
"value": "#4C78A8"
}
},
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"x": { "scale": "xscale", "field": "x" }, "value": "monotone"
"y": { "scale": "yscale", "field": "p1" }, },
"y2": { "scale": "yscale", "field": "p99" }, "x": {
"opacity": { "value": 0.05 } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p1"
},
"y2": {
"scale": "yscale",
"field": "p99"
},
"opacity": {
"value": 0.05
}
} }
} }
}, },
{ {
"type": "area", "type": "area",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"enter": { "fill": { "value": "#4C78A8" } }, "enter": {
"fill": {
"value": "#4C78A8"
}
},
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"x": { "scale": "xscale", "field": "x" }, "value": "monotone"
"y": { "scale": "yscale", "field": "p5" }, },
"y2": { "scale": "yscale", "field": "p95" }, "x": {
"opacity": { "value": 0.1 } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p5"
},
"y2": {
"scale": "yscale",
"field": "p95"
},
"opacity": {
"value": 0.1
}
} }
} }
}, },
{ {
"type": "area", "type": "area",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"enter": { "fill": { "value": "#4C78A8" } }, "enter": {
"fill": {
"value": "#4C78A8"
}
},
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"x": { "scale": "xscale", "field": "x" }, "value": "monotone"
"y": { "scale": "yscale", "field": "p10" }, },
"y2": { "scale": "yscale", "field": "p90" }, "x": {
"opacity": { "value": 0.15 } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p10"
},
"y2": {
"scale": "yscale",
"field": "p90"
},
"opacity": {
"value": 0.15
}
} }
} }
}, },
{ {
"type": "area", "type": "area",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"enter": { "fill": { "value": "#4C78A8" } }, "enter": {
"fill": {
"value": "#4C78A8"
}
},
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"x": { "scale": "xscale", "field": "x" }, "value": "monotone"
"y": { "scale": "yscale", "field": "p20" }, },
"y2": { "scale": "yscale", "field": "p80" }, "x": {
"opacity": { "value": 0.2 } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p20"
},
"y2": {
"scale": "yscale",
"field": "p80"
},
"opacity": {
"value": 0.2
}
} }
} }
}, },
{ {
"type": "area", "type": "area",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"enter": { "fill": { "value": "#4C78A8" } }, "enter": {
"fill": {
"value": "#4C78A8"
}
},
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"x": { "scale": "xscale", "field": "x" }, "value": "monotone"
"y": { "scale": "yscale", "field": "p30" }, },
"y2": { "scale": "yscale", "field": "p70" }, "x": {
"opacity": { "value": 0.2 } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p30"
},
"y2": {
"scale": "yscale",
"field": "p70"
},
"opacity": {
"value": 0.2
}
} }
} }
}, },
{ {
"type": "area", "type": "area",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"enter": { "fill": { "value": "#4C78A8" } }, "enter": {
"fill": {
"value": "#4C78A8"
}
},
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"x": { "scale": "xscale", "field": "x" }, "value": "monotone"
"y": { "scale": "yscale", "field": "p40" }, },
"y2": { "scale": "yscale", "field": "p60" }, "x": {
"opacity": { "value": 0.2 } "scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p40"
},
"y2": {
"scale": "yscale",
"field": "p60"
},
"opacity": {
"value": 0.2
}
} }
} }
}, },
{ {
"type": "line", "type": "line",
"from": { "data": "table" }, "from": {
"data": "table"
},
"encode": { "encode": {
"update": { "update": {
"interpolate": { "value": "monotone" }, "interpolate": {
"stroke": { "value": "#4C78A8" }, "value": "monotone"
"strokeWidth": { "value": 2 }, },
"opacity": { "value": 0.8 }, "stroke": {
"x": { "scale": "xscale", "field": "x" }, "value": "#4C78A8"
"y": { "scale": "yscale", "field": "p50" } },
"strokeWidth": {
"value": 2
},
"opacity": {
"value": 0.8
},
"x": {
"scale": "xscale",
"field": "x"
},
"y": {
"scale": "yscale",
"field": "p50"
}
} }
} }
} }

View File

@ -2,8 +2,5 @@ import { Meta } from "@storybook/addon-docs";
<Meta title="Squiggle/Introduction" /> <Meta title="Squiggle/Introduction" />
This is the component library for Squiggle. All of these components are react This is the component library for Squiggle. These are React
components, and can be used in any application that you see fit. 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.

View File

@ -0,0 +1,60 @@
import { NumberShower } from "../NumberShower";
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
<Meta title="Squiggle/NumberShower" component={NumberShower} />
# Number Shower
The number shower is a simple component to display a number.
It uses the symbols "K", "M", "B", and "T", to represent thousands, millions, billions, and trillions. Outside of that range, it uses scientific notation.
<Canvas>
<Story
name="Ten Thousand"
args={{
number: 10000,
precision: 2
}}
>
{args => <NumberShower {...args}/>}
</Story>
</Canvas>
<Canvas>
<Story
name="Ten Billion"
args={{
number: 10000000000,
precision: 2
}}
>
{args => <NumberShower {...args}/>}
</Story>
</Canvas>
<Canvas>
<Story
name="1.2*10^15"
args={{
number: 1200000000000000,
precision: 2
}}
>
{args => <NumberShower {...args}/>}
</Story>
</Canvas>
<Canvas>
<Story
name="1.35*10^-13"
args={{
number: 0.000000000000135,
precision: 2
}}
>
{args => <NumberShower {...args}/>}
</Story>
</Canvas>
<Props of={NumberShower} />

View File

@ -18,7 +18,7 @@ could be continuous, discrete or mixed.
## Distributions ## Distributions
An example of a normal distribution is: ### Continuous Distributions
<Canvas> <Canvas>
<Story <Story
@ -31,26 +31,26 @@ An example of a normal distribution is:
</Story> </Story>
</Canvas> </Canvas>
An example of a Discrete distribution is: ### Discrete Distributions
<Canvas> <Canvas>
<Story <Story
name="Discrete" name="Discrete"
args={{ args={{
squiggleString: "mm(0, 1, [0.5, 0.5])", squiggleString: "mm(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])",
}} }}
> >
{Template.bind({})} {Template.bind({})}
</Story> </Story>
</Canvas> </Canvas>
An example of a Mixed distribution is: ## Mixed distributions
<Canvas> <Canvas>
<Story <Story
name="Mixed" name="Mixed"
args={{ args={{
squiggleString: "mm(0, 5 to 10, [0.5, 0.5])", squiggleString: "mm(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])",
}} }}
> >
{Template.bind({})} {Template.bind({})}
@ -66,7 +66,7 @@ to allow large and small numbers being printed cleanly.
<Story <Story
name="Constant" name="Constant"
args={{ args={{
squiggleString: "500000 * 5000000", squiggleString: "500000000",
}} }}
> >
{Template.bind({})} {Template.bind({})}
@ -75,14 +75,15 @@ to allow large and small numbers being printed cleanly.
## Functions ## Functions
Finally, a function can be returned, and this shows how the distribution changes Full functions can be returned. These plot out the results of distributions between a set of x-coordinates.
over the axis between x = 0 and 10.
The default is show 10 points between 0 and 10.
<Canvas> <Canvas>
<Story <Story
name="Function" name="Function"
args={{ args={{
squiggleString: "f(x) = normal(x,x)\nf", squiggleString: "f(x) = normal(x^2,x^1.8)\nf",
}} }}
> >
{Template.bind({})} {Template.bind({})}