combined distribution chart component
This commit is contained in:
parent
7c00897833
commit
0292c66c80
|
@ -15,7 +15,6 @@ import {
|
||||||
buildVegaSpec,
|
buildVegaSpec,
|
||||||
DistributionChartSpecOptions,
|
DistributionChartSpecOptions,
|
||||||
} from "../lib/distributionSpecBuilder";
|
} from "../lib/distributionSpecBuilder";
|
||||||
import { buildDateVegaSpec } from "../lib/dateDistributionSpecBuilder";
|
|
||||||
import { NumberShower } from "./NumberShower";
|
import { NumberShower } from "./NumberShower";
|
||||||
import { Plot, parsePlot } from "../lib/plotParser";
|
import { Plot, parsePlot } from "../lib/plotParser";
|
||||||
import { flattenResult } from "../lib/utility";
|
import { flattenResult } from "../lib/utility";
|
||||||
|
@ -31,6 +30,7 @@ export type DistributionChartProps = {
|
||||||
plot: Plot;
|
plot: Plot;
|
||||||
width?: number;
|
width?: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
sample: boolean;
|
||||||
xAxis?: "number" | "dateTime";
|
xAxis?: "number" | "dateTime";
|
||||||
} & DistributionPlottingSettings;
|
} & DistributionPlottingSettings;
|
||||||
|
|
||||||
|
@ -77,8 +77,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const spec =
|
const spec = buildVegaSpec(props);
|
||||||
xAxis === "dateTime" ? buildDateVegaSpec(props) : buildVegaSpec(props);
|
|
||||||
|
|
||||||
let widthProp = width ? width : size.width;
|
let widthProp = width ? width : size.width;
|
||||||
if (widthProp < 20) {
|
if (widthProp < 20) {
|
||||||
|
|
|
@ -1,384 +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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const timeXScale: TimeScale = {
|
|
||||||
name: "xscale",
|
|
||||||
clamp: true,
|
|
||||||
type: "time",
|
|
||||||
range: "width",
|
|
||||||
nice: false,
|
|
||||||
domain: { data: "domain", field: "dateTime" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const linearYScale: LinearScale = {
|
|
||||||
name: "yscale",
|
|
||||||
type: "linear",
|
|
||||||
range: "height",
|
|
||||||
zero: true,
|
|
||||||
domain: { data: "domain", field: "y" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logXScale: LogScale = {
|
|
||||||
name: "xscale",
|
|
||||||
type: "log",
|
|
||||||
range: "width",
|
|
||||||
zero: false,
|
|
||||||
base: 10,
|
|
||||||
nice: false,
|
|
||||||
clamp: true,
|
|
||||||
domain: { data: "domain", field: "x" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const expYScale: PowScale = {
|
|
||||||
name: "yscale",
|
|
||||||
type: "pow",
|
|
||||||
exponent: 0.1,
|
|
||||||
range: "height",
|
|
||||||
zero: true,
|
|
||||||
nice: false,
|
|
||||||
domain: { data: "domain", field: "y" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultTickFormat = "%b %d, %Y %H:%M";
|
|
||||||
|
|
||||||
export function buildDateVegaSpec(
|
|
||||||
specOptions: DistributionChartSpecOptions
|
|
||||||
): VisualizationSpec {
|
|
||||||
const {
|
|
||||||
format = defaultTickFormat,
|
|
||||||
title,
|
|
||||||
minX,
|
|
||||||
maxX,
|
|
||||||
logX,
|
|
||||||
expY,
|
|
||||||
} = specOptions;
|
|
||||||
|
|
||||||
let xScale = timeXScale;
|
|
||||||
if (minX !== undefined && Number.isFinite(minX)) {
|
|
||||||
xScale = { ...xScale, domainMin: minX };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxX !== undefined && Number.isFinite(maxX)) {
|
|
||||||
xScale = { ...xScale, domainMax: maxX };
|
|
||||||
}
|
|
||||||
|
|
||||||
const spec: VisualizationSpec = {
|
|
||||||
$schema: "https://vega.github.io/schema/vega/v5.json",
|
|
||||||
description: "Squiggle plot chart",
|
|
||||||
width: 500,
|
|
||||||
height: 100,
|
|
||||||
padding: 5,
|
|
||||||
data: [{ name: "data" }, { name: "domain" }],
|
|
||||||
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,
|
|
||||||
expY ? expYScale : linearYScale,
|
|
||||||
{
|
|
||||||
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: 3,
|
|
||||||
labelOverlap: "greedy",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
marks: [
|
|
||||||
{
|
|
||||||
name: "sample_distributions",
|
|
||||||
type: "group",
|
|
||||||
from: {
|
|
||||||
facet: {
|
|
||||||
name: "distribution_facet",
|
|
||||||
data: "domain",
|
|
||||||
groupby: ["name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
marks: [
|
|
||||||
{
|
|
||||||
name: "samples",
|
|
||||||
type: "rect",
|
|
||||||
from: { data: "distribution_facet" },
|
|
||||||
encode: {
|
|
||||||
enter: {
|
|
||||||
x: { scale: "xscale", field: "dateTime" },
|
|
||||||
width: { value: 0.5 },
|
|
||||||
|
|
||||||
y: { value: 25, offset: { signal: "height" } },
|
|
||||||
height: { value: 5 },
|
|
||||||
fill: { value: "steelblue" },
|
|
||||||
fillOpacity: { value: 0.8 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: "dateTime",
|
|
||||||
},
|
|
||||||
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: "dateTime",
|
|
||||||
},
|
|
||||||
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:
|
|
||||||
"{ probability: datum.y, value: datetime(datum.dateTime) }",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
x: {
|
|
||||||
scale: "xscale",
|
|
||||||
field: "dateTime",
|
|
||||||
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" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
interactive: false,
|
|
||||||
encode: {
|
|
||||||
enter: {
|
|
||||||
text: {
|
|
||||||
signal: "",
|
|
||||||
},
|
|
||||||
x: { signal: "width", offset: 1 },
|
|
||||||
fill: { value: "black" },
|
|
||||||
fontSize: { value: 20 },
|
|
||||||
align: { value: "right" },
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
text: {
|
|
||||||
signal:
|
|
||||||
"position_scaled ? utcyear(position_scaled) + '-' + utcmonth(position_scaled) + '-' + utcdate(position_scaled) + 'T' + utchours(position_scaled)+':' +utcminutes(position_scaled) : ''",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "rule",
|
|
||||||
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 ? 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;
|
|
||||||
}
|
|
|
@ -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,6 +14,12 @@ export type DistributionChartSpecOptions = {
|
||||||
title?: string;
|
title?: string;
|
||||||
/** The formatting of the ticks */
|
/** The formatting of the ticks */
|
||||||
format?: string;
|
format?: string;
|
||||||
|
|
||||||
|
/** Whether or not to show the band of sample data at the bottom */
|
||||||
|
sample?: boolean;
|
||||||
|
|
||||||
|
/** Whether the x-axis should be dates or numbers */
|
||||||
|
xAxis?: "number" | "dateTime";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const linearXScale: LinearScale = {
|
export const linearXScale: LinearScale = {
|
||||||
|
@ -26,14 +32,6 @@ export const linearXScale: LinearScale = {
|
||||||
domain: { data: "domain", field: "x" },
|
domain: { data: "domain", field: "x" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const linearYScale: LinearScale = {
|
|
||||||
name: "yscale",
|
|
||||||
type: "linear",
|
|
||||||
range: "height",
|
|
||||||
zero: true,
|
|
||||||
domain: { data: "domain", field: "y" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logXScale: LogScale = {
|
export const logXScale: LogScale = {
|
||||||
name: "xscale",
|
name: "xscale",
|
||||||
type: "log",
|
type: "log",
|
||||||
|
@ -45,6 +43,23 @@ export const logXScale: LogScale = {
|
||||||
domain: { data: "domain", field: "x" },
|
domain: { data: "domain", field: "x" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const timeXScale: TimeScale = {
|
||||||
|
name: "xscale",
|
||||||
|
clamp: true,
|
||||||
|
type: "time",
|
||||||
|
range: "width",
|
||||||
|
nice: false,
|
||||||
|
domain: { data: "domain", field: "dateTime" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const linearYScale: LinearScale = {
|
||||||
|
name: "yscale",
|
||||||
|
type: "linear",
|
||||||
|
range: "height",
|
||||||
|
zero: true,
|
||||||
|
domain: { data: "domain", field: "y" },
|
||||||
|
};
|
||||||
|
|
||||||
export const expYScale: PowScale = {
|
export const expYScale: PowScale = {
|
||||||
name: "yscale",
|
name: "yscale",
|
||||||
type: "pow",
|
type: "pow",
|
||||||
|
@ -56,6 +71,7 @@ export const expYScale: PowScale = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultTickFormat = ".9~s";
|
export const defaultTickFormat = ".9~s";
|
||||||
|
export const timeTickFormat = "%b %d, %Y %H:%M";
|
||||||
|
|
||||||
export function buildVegaSpec(
|
export function buildVegaSpec(
|
||||||
specOptions: DistributionChartSpecOptions
|
specOptions: DistributionChartSpecOptions
|
||||||
|
@ -67,9 +83,14 @@ export function buildVegaSpec(
|
||||||
maxX,
|
maxX,
|
||||||
logX,
|
logX,
|
||||||
expY,
|
expY,
|
||||||
|
sample = false,
|
||||||
|
xAxis = "number",
|
||||||
} = specOptions;
|
} = specOptions;
|
||||||
|
|
||||||
let xScale = logX ? logXScale : linearXScale;
|
const dateTime = xAxis === "dateTime";
|
||||||
|
|
||||||
|
let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale;
|
||||||
|
|
||||||
if (minX !== undefined && Number.isFinite(minX)) {
|
if (minX !== undefined && Number.isFinite(minX)) {
|
||||||
xScale = { ...xScale, domainMin: minX };
|
xScale = { ...xScale, domainMin: minX };
|
||||||
}
|
}
|
||||||
|
@ -105,9 +126,7 @@ export function buildVegaSpec(
|
||||||
{
|
{
|
||||||
name: "position_scaled",
|
name: "position_scaled",
|
||||||
value: 0,
|
value: 0,
|
||||||
update: "position ? position[0] < 0 ? null : position[0] > width ? null : invert('xscale', position[0]) : null",
|
update: "position ? invert('xscale', position[0]) : null",
|
||||||
// "position ? position[0] < 0 ? 0 : position[0] > width ? 0 : 1 : 0",
|
|
||||||
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
scales: [
|
scales: [
|
||||||
|
@ -138,7 +157,6 @@ export function buildVegaSpec(
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
marks: [
|
marks: [
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "all_distributions",
|
name: "all_distributions",
|
||||||
type: "group",
|
type: "group",
|
||||||
|
@ -175,7 +193,7 @@ export function buildVegaSpec(
|
||||||
interpolate: { value: "linear" },
|
interpolate: { value: "linear" },
|
||||||
x: {
|
x: {
|
||||||
scale: "xscale",
|
scale: "xscale",
|
||||||
field: "x",
|
field: dateTime ? "dateTime" : "x",
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
scale: "yscale",
|
scale: "yscale",
|
||||||
|
@ -222,7 +240,7 @@ export function buildVegaSpec(
|
||||||
update: {
|
update: {
|
||||||
x: {
|
x: {
|
||||||
scale: "xscale",
|
scale: "xscale",
|
||||||
field: "x",
|
field: dateTime ? "dateTime" : "x",
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
scale: "yscale",
|
scale: "yscale",
|
||||||
|
@ -251,13 +269,15 @@ 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.dateTime) }"
|
||||||
|
: "{ probability: datum.y, value: datum.x }",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
x: {
|
x: {
|
||||||
scale: "xscale",
|
scale: "xscale",
|
||||||
field: "x",
|
field: dateTime ? "dateTime" : "x",
|
||||||
offset: 0.5, // if this is not included, the circles are slightly left of center.
|
offset: 0.5, // if this is not included, the circles are slightly left of center.
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
|
@ -356,5 +376,37 @@ export function buildVegaSpec(
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// include the band at the bottom if specified in the React component
|
||||||
|
if (sample) {
|
||||||
|
spec.marks?.push({
|
||||||
|
name: "sample_distributions",
|
||||||
|
type: "group",
|
||||||
|
from: {
|
||||||
|
facet: {
|
||||||
|
name: "distribution_facet",
|
||||||
|
data: "domain",
|
||||||
|
groupby: ["name"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
name: "samples",
|
||||||
|
type: "rect",
|
||||||
|
from: { data: "distribution_facet" },
|
||||||
|
encode: {
|
||||||
|
enter: {
|
||||||
|
x: { scale: "xscale", field: dateTime ? "dateTime" : "x" },
|
||||||
|
width: { value: 0.5 },
|
||||||
|
|
||||||
|
y: { value: 25, offset: { signal: "height" } },
|
||||||
|
height: { value: 5 },
|
||||||
|
fill: { value: "steelblue" },
|
||||||
|
fillOpacity: { value: 0.8 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user