Merge pull request #1008 from ideopunk/feature/distribution_tweaks
Distribution component tweaks
This commit is contained in:
commit
45323b0e08
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -7,4 +7,7 @@ yarn-error.log
|
||||||
**/.sync.ffs_db
|
**/.sync.ffs_db
|
||||||
.direnv
|
.direnv
|
||||||
.log
|
.log
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
todo.txt
|
||||||
result
|
result
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
resultMap,
|
resultMap,
|
||||||
SqRecord,
|
SqRecord,
|
||||||
environment,
|
environment,
|
||||||
|
SqDistributionTag,
|
||||||
} from "@quri/squiggle-lang";
|
} from "@quri/squiggle-lang";
|
||||||
import { Vega } from "react-vega";
|
import { Vega } from "react-vega";
|
||||||
import { ErrorAlert } from "./Alert";
|
import { ErrorAlert } from "./Alert";
|
||||||
|
@ -31,6 +32,7 @@ export type DistributionChartProps = {
|
||||||
environment: environment;
|
environment: environment;
|
||||||
width?: number;
|
width?: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
xAxisType?: "number" | "dateTime";
|
||||||
} & DistributionPlottingSettings;
|
} & DistributionPlottingSettings;
|
||||||
|
|
||||||
export function defaultPlot(distribution: SqDistribution): Plot {
|
export function defaultPlot(distribution: SqDistribution): Plot {
|
||||||
|
@ -56,14 +58,15 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
||||||
} = props;
|
} = props;
|
||||||
const [sized] = useSize((size) => {
|
const [sized] = useSize((size) => {
|
||||||
const shapes = flattenResult(
|
const shapes = flattenResult(
|
||||||
plot.distributions.map((x) => {
|
plot.distributions.map((x) =>
|
||||||
return resultMap(x.distribution.pointSet(environment), (pointSet) => ({
|
resultMap(x.distribution.pointSet(environment), (pointSet) => ({
|
||||||
...pointSet.asShape(),
|
|
||||||
name: x.name,
|
name: x.name,
|
||||||
// color: x.color, // not supported yet
|
// color: x.color, // not supported yet
|
||||||
}));
|
...pointSet.asShape(),
|
||||||
})
|
}))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shapes.tag === "Error") {
|
if (shapes.tag === "Error") {
|
||||||
return (
|
return (
|
||||||
<ErrorAlert heading="Distribution Error">
|
<ErrorAlert heading="Distribution Error">
|
||||||
|
@ -72,6 +75,14 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 spec = buildVegaSpec(props);
|
const spec = buildVegaSpec(props);
|
||||||
|
|
||||||
let widthProp = width ? width : size.width;
|
let widthProp = width ? width : size.width;
|
||||||
|
@ -94,7 +105,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
||||||
) : (
|
) : (
|
||||||
<Vega
|
<Vega
|
||||||
spec={spec}
|
spec={spec}
|
||||||
data={{ data: shapes.value, domain }}
|
data={{ data: shapes.value, domain, samples }}
|
||||||
width={widthProp - 10}
|
width={widthProp - 10}
|
||||||
height={height}
|
height={height}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
|
|
@ -48,6 +48,8 @@ export interface SquiggleChartProps {
|
||||||
minX?: number;
|
minX?: number;
|
||||||
/** Specify the upper bound of the x scale */
|
/** Specify the upper bound of the x scale */
|
||||||
maxX?: number;
|
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 */
|
/** Whether to show vega actions to the user, so they can copy the chart spec */
|
||||||
distributionChartActions?: boolean;
|
distributionChartActions?: boolean;
|
||||||
enableLocalSettings?: boolean;
|
enableLocalSettings?: boolean;
|
||||||
|
@ -76,6 +78,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
||||||
maxX,
|
maxX,
|
||||||
color,
|
color,
|
||||||
title,
|
title,
|
||||||
|
xAxisType = "number",
|
||||||
distributionChartActions,
|
distributionChartActions,
|
||||||
enableLocalSettings = false,
|
enableLocalSettings = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -96,6 +99,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
||||||
maxX,
|
maxX,
|
||||||
color,
|
color,
|
||||||
title,
|
title,
|
||||||
|
xAxisType,
|
||||||
actions: distributionChartActions,
|
actions: distributionChartActions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { VisualizationSpec } from "react-vega";
|
import { VisualizationSpec } from "react-vega";
|
||||||
import type { LogScale, LinearScale, PowScale } from "vega";
|
import type { LogScale, LinearScale, PowScale, TimeScale } from "vega";
|
||||||
|
|
||||||
export type DistributionChartSpecOptions = {
|
export type DistributionChartSpecOptions = {
|
||||||
/** Set the x scale to be logarithmic by deault */
|
/** Set the x scale to be logarithmic by deault */
|
||||||
|
@ -14,9 +14,12 @@ export type DistributionChartSpecOptions = {
|
||||||
title?: string;
|
title?: string;
|
||||||
/** The formatting of the ticks */
|
/** The formatting of the ticks */
|
||||||
format?: string;
|
format?: string;
|
||||||
|
/** Whether the x-axis should be dates or numbers */
|
||||||
|
xAxisType?: "number" | "dateTime";
|
||||||
};
|
};
|
||||||
|
|
||||||
export let linearXScale: LinearScale = {
|
/** X Scales */
|
||||||
|
export const linearXScale: LinearScale = {
|
||||||
name: "xscale",
|
name: "xscale",
|
||||||
clamp: true,
|
clamp: true,
|
||||||
type: "linear",
|
type: "linear",
|
||||||
|
@ -25,15 +28,8 @@ export let linearXScale: LinearScale = {
|
||||||
nice: false,
|
nice: false,
|
||||||
domain: { data: "domain", field: "x" },
|
domain: { data: "domain", field: "x" },
|
||||||
};
|
};
|
||||||
export let linearYScale: LinearScale = {
|
|
||||||
name: "yscale",
|
|
||||||
type: "linear",
|
|
||||||
range: "height",
|
|
||||||
zero: true,
|
|
||||||
domain: { data: "domain", field: "y" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export let logXScale: LogScale = {
|
export const logXScale: LogScale = {
|
||||||
name: "xscale",
|
name: "xscale",
|
||||||
type: "log",
|
type: "log",
|
||||||
range: "width",
|
range: "width",
|
||||||
|
@ -44,7 +40,25 @@ export let logXScale: LogScale = {
|
||||||
domain: { data: "domain", field: "x" },
|
domain: { data: "domain", field: "x" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export let expYScale: PowScale = {
|
export const timeXScale: TimeScale = {
|
||||||
|
name: "xscale",
|
||||||
|
clamp: true,
|
||||||
|
type: "time",
|
||||||
|
range: "width",
|
||||||
|
nice: false,
|
||||||
|
domain: { data: "domain", field: "x" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Y Scales */
|
||||||
|
export const linearYScale: LinearScale = {
|
||||||
|
name: "yscale",
|
||||||
|
type: "linear",
|
||||||
|
range: "height",
|
||||||
|
zero: true,
|
||||||
|
domain: { data: "domain", field: "y" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expYScale: PowScale = {
|
||||||
name: "yscale",
|
name: "yscale",
|
||||||
type: "pow",
|
type: "pow",
|
||||||
exponent: 0.1,
|
exponent: 0.1,
|
||||||
|
@ -55,20 +69,25 @@ export let expYScale: PowScale = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultTickFormat = ".9~s";
|
export const defaultTickFormat = ".9~s";
|
||||||
|
export const timeTickFormat = "%b %d, %Y %H:%M";
|
||||||
|
const width = 500;
|
||||||
|
|
||||||
export function buildVegaSpec(
|
export function buildVegaSpec(
|
||||||
specOptions: DistributionChartSpecOptions
|
specOptions: DistributionChartSpecOptions
|
||||||
): VisualizationSpec {
|
): VisualizationSpec {
|
||||||
const {
|
const { title, minX, maxX, logX, expY, xAxisType = "number" } = specOptions;
|
||||||
format = defaultTickFormat,
|
|
||||||
title,
|
const dateTime = xAxisType === "dateTime";
|
||||||
minX,
|
|
||||||
maxX,
|
// some fallbacks
|
||||||
logX,
|
const format = specOptions?.format
|
||||||
expY,
|
? specOptions.format
|
||||||
} = specOptions;
|
: dateTime
|
||||||
|
? timeTickFormat
|
||||||
|
: defaultTickFormat;
|
||||||
|
|
||||||
|
let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale;
|
||||||
|
|
||||||
let xScale = logX ? logXScale : linearXScale;
|
|
||||||
if (minX !== undefined && Number.isFinite(minX)) {
|
if (minX !== undefined && Number.isFinite(minX)) {
|
||||||
xScale = { ...xScale, domainMin: minX };
|
xScale = { ...xScale, domainMin: minX };
|
||||||
}
|
}
|
||||||
|
@ -77,21 +96,36 @@ export function buildVegaSpec(
|
||||||
xScale = { ...xScale, domainMax: maxX };
|
xScale = { ...xScale, domainMax: maxX };
|
||||||
}
|
}
|
||||||
|
|
||||||
let spec: VisualizationSpec = {
|
const spec: VisualizationSpec = {
|
||||||
$schema: "https://vega.github.io/schema/vega/v5.json",
|
$schema: "https://vega.github.io/schema/vega/v5.json",
|
||||||
description: "Squiggle plot chart",
|
description: "Squiggle plot chart",
|
||||||
width: 500,
|
width: width,
|
||||||
height: 100,
|
height: 100,
|
||||||
padding: 5,
|
padding: 5,
|
||||||
data: [
|
data: [{ name: "data" }, { name: "domain" }, { name: "samples" }],
|
||||||
|
signals: [
|
||||||
{
|
{
|
||||||
name: "data",
|
name: "hover",
|
||||||
|
value: null,
|
||||||
|
on: [
|
||||||
|
{ events: "mouseover", update: "datum" },
|
||||||
|
{ events: "mouseout", update: "null" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "domain",
|
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]) : ''",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
signals: [],
|
|
||||||
scales: [
|
scales: [
|
||||||
xScale,
|
xScale,
|
||||||
expY ? expYScale : linearYScale,
|
expY ? expYScale : linearYScale,
|
||||||
|
@ -115,7 +149,7 @@ export function buildVegaSpec(
|
||||||
domainColor: "#fff",
|
domainColor: "#fff",
|
||||||
domainOpacity: 0.0,
|
domainOpacity: 0.0,
|
||||||
format: format,
|
format: format,
|
||||||
tickCount: 10,
|
tickCount: dateTime ? 3 : 10,
|
||||||
labelOverlap: "greedy",
|
labelOverlap: "greedy",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -232,13 +266,16 @@ export function buildVegaSpec(
|
||||||
},
|
},
|
||||||
size: [{ value: 100 }],
|
size: [{ value: 100 }],
|
||||||
tooltip: {
|
tooltip: {
|
||||||
signal: "{ probability: datum.y, value: datum.x }",
|
signal: dateTime
|
||||||
|
? "{ probability: datum.y, value: datetime(datum.x) }"
|
||||||
|
: "{ probability: datum.y, value: datum.x }",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
x: {
|
x: {
|
||||||
scale: "xscale",
|
scale: "xscale",
|
||||||
field: "x",
|
field: "x",
|
||||||
|
offset: 0.5, // if this is not included, the circles are slightly left of center.
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
scale: "yscale",
|
scale: "yscale",
|
||||||
|
@ -255,6 +292,69 @@ export function buildVegaSpec(
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
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: [
|
legends: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -79,6 +79,22 @@ could be continuous, discrete or mixed.
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
### Date Distribution
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Date Distribution"
|
||||||
|
args={{
|
||||||
|
code: "mx(1661819770311, 1661829770311, 1661839770311)",
|
||||||
|
width,
|
||||||
|
xAxisType: "dateTime",
|
||||||
|
width,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
## Mixed distributions
|
## Mixed distributions
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
|
|
|
@ -80,27 +80,28 @@ abstract class SqAbstractDistribution {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqPointSetDistribution extends SqAbstractDistribution {
|
export class SqPointSetDistribution extends SqAbstractDistribution {
|
||||||
tag = Tag.PointSet;
|
tag = Tag.PointSet as const;
|
||||||
|
|
||||||
value() {
|
value() {
|
||||||
return this.valueMethod(RSDistribution.getPointSet);
|
return wrapPointSetDist(this.valueMethod(RSDistribution.getPointSet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqSampleSetDistribution extends SqAbstractDistribution {
|
export class SqSampleSetDistribution extends SqAbstractDistribution {
|
||||||
tag = Tag.SampleSet;
|
tag = Tag.SampleSet as const;
|
||||||
|
|
||||||
value() {
|
value(): number[] {
|
||||||
return this.valueMethod(RSDistribution.getSampleSet);
|
return this.valueMethod(RSDistribution.getSampleSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqSymbolicDistribution extends SqAbstractDistribution {
|
export class SqSymbolicDistribution extends SqAbstractDistribution {
|
||||||
tag = Tag.Symbolic;
|
tag = Tag.Symbolic as const;
|
||||||
|
|
||||||
value() {
|
// not wrapped for TypeScript yet
|
||||||
return this.valueMethod(RSDistribution.getSymbolic);
|
// value() {
|
||||||
}
|
// return this.valueMethod(RSDistribution.getSymbolic);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagToClass = {
|
const tagToClass = {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user