diff --git a/playground/src/main.ts b/playground/src/main.ts index ff5a501..84f3e6f 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,18 +1,18 @@ -import { chartXy, arrow, chartDonut, chartGauge, clipPath, path, use, findArcMidpoint, ChartXyDatasetItem } from "savyg"; +import { chartXy, arrow, chartDonut, text, chartGauge, clipPath, path, use, findArcMidpoint, ChartXyDatasetItem } from "savyg"; // const parent = document.getElementById("svg") as HTMLElement const div = document.getElementById("div") as HTMLElement const gaugeDs = { - value: 4.56, + value: "4.56", segments: [ { - from: 0, - to: 1 + from: "0", + to: "1" }, { - from: 1, - to: 2 + from: "1", + to: "2" }, { from: 2, @@ -34,6 +34,8 @@ let gauge1 = chartGauge({ options: { title: "Title", valueRounding: 1, + pointerSize: 1, + pointerWidth: 12 }, parent: div }) @@ -90,7 +92,8 @@ const xyDataset = [ plotRadius: 0, gradientFrom: "#FF000033", gradientTo: "#0000FF33", - rx: 3 + rx: 3, + dataLabelsColor: "red" } ] as ChartXyDatasetItem[] @@ -98,11 +101,32 @@ let xy = chartXy({ dataset: xyDataset, parent: div, options: { + axisColor: "#000000", + backgroundColor: "#FFFFFF", + fontFamily: "inherit", barSpacing: 2, - showAxis: true, xAxisLabels: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"], + zoomColor: "#0000FF10", + gridColor: "#CCCCCC", + interactive: true, + legendColor: "#000000", + legendFontSize: 10, + paddingBottom: 48, + paddingLeft: 48, + paddingRight: 24, + paddingTop: 48, + selectorColor: "#FF000010", + "shape-rendering": "auto", + showAxis: true, + showGrid: true, + showLegend: true, title: "Title", - zoomColor: "#0000FF10" + titleColor: "#000000", + titleFontSize: 18, + titlePosition: "start", + tooltipBackgroundColor: "#FFFFFF", + tooltipColor: "#000000", + viewBox: "0 0 512 341" }, callbacks: { onClickLegend: xyCb, @@ -124,43 +148,11 @@ let donut = chartDonut({ dataset: [ { name: "serie 1", - value: 0.1, + value: "12", }, { name: "serie 1.1", - value: 0 / 1, - }, - { - name: "serie 1.3", - value: 0.1, - }, - { - name: "serie 1.3", - value: 0.1, - }, - { - name: "serie 1.3", - value: 0.1, - }, - { - name: "serie 1.3", - value: 0.1, - }, - { - name: "serie 2", - value: 10, - }, - { - name: "serie 3", - value: 10, - }, - { - name: "serie 4", - value: 20, - }, - { - name: "serie 4", - value: 20, + value: 12, }, ], parent: div, @@ -180,7 +172,6 @@ let donut = chartDonut({ } }) -// console.log(findArcMidpoint(donut.arcs[0].pathElement)) const nuke = document.getElementById('nuke'); @@ -220,6 +211,6 @@ const genDs = document.getElementById('genDs') genDs?.addEventListener('click', () => { donut = donut.updateData(makeRandomDonutDataset()) - gauge1 = gauge1.updateData(makeRandomGaugeDataset()) xy = xy.updateData(makeRandomXyDataset()) + gauge1 = gauge1.updateData(makeRandomGaugeDataset()) }) \ No newline at end of file diff --git a/savyg/package.json b/savyg/package.json index a80bf49..92e73f6 100644 --- a/savyg/package.json +++ b/savyg/package.json @@ -1,7 +1,7 @@ { "name": "savyg", "private": false, - "version": "1.2.1", + "version": "1.2.2", "description": "A savvy library to create svg elements and charts with ease", "author": "Alec Lloyd Probert", "repository": { diff --git a/savyg/src/utils_chart_donut.ts b/savyg/src/utils_chart_donut.ts index 96facf7..0be0d07 100644 --- a/savyg/src/utils_chart_donut.ts +++ b/savyg/src/utils_chart_donut.ts @@ -1,5 +1,5 @@ import { palette } from "./palette" -import { createUid, fordinum, getSvgDimensions, makeDonut } from "./utils_common" +import { createUid, forceNum, fordinum, getSvgDimensions, makeDonut } from "./utils_common" import { circle, element, findArcMidpoint, line, offsetFromCenterPoint, path, setTextAnchorFromCenterPoint, svg, text } from "./utils_svg" import { ChartArea, DrawingArea, ShapeRendering, StrokeOptions, SvgItem, TextAnchor } from "./utils_svg_types" @@ -10,44 +10,183 @@ export type ChartDonutDatasetItem = StrokeOptions & { } export type ChartDonutOptions = { + /** + * @option the background color applied to the chart + * @default "#FFFFFF" + */ backgroundColor?: string + /** + * @option pass any strings separated by a space to generate class names. Example: "my-class1 my-class2" + */ className?: string + /** + * @option display data labels as divs inside a foreignObject, to get more control on the styling (line breaks, etc). If false, displays the data labels as a svg text element, with no control over line breaks. + * @default false + */ dataLabelsAsDivs?: boolean + /** + * @option the text color of data labels + * @default "#000000" + */ dataLabelsColor?: string + /** + * @option the font size of data labels + * @default 12 + */ dataLabelsFontSize?: number + /** + * @option the rounding of the percentage displayed in data labels + * @default 0 + */ dataLabelsRoundingPercentage?: number + /** + * @option the rounding of the value displayed in data labels + * @default 0 + */ dataLabelsRoundingValue?: number + /** + * @option the offset of data labels from the arcs + * @default 40 + */ dataLabelsOffset?: number + /** + * @option the offset of the data labels line markers + * @default 20 + */ dataLabelsLineOffset?: number + /** + * @option the border width of the donut arcs + * @default 1 + */ donutBorderWidth?: number + /** + * @option the size ratio of the overall donut radius + * @default 1 + * @example 0.8 will make it smaller + */ donutRadiusRatio?: number + /** + * @option the thickness of donut arcs + * @default 48 + */ donutThickness?: number + /** + * @option font family for all text elements + * @default "inherit" + */ fontFamily?: string + /** + * @option values under this threshold will be displayed in smaller font and stacked to the top of the chart + * @default 3 + */ hideLabelUnderPercentage?: number + /** + * @option the id of the svg. Defaults to a random uid + */ id?: string + /** + * @option activates user interactions (tooltip) + * @default true + */ interactive?: boolean + /** + * @option the text color of legend elements + * @default "#000000" + */ legendColor?: string + /** + * @option the font size of legend elements + * @default 10 + */ legendFontSize?: number + /** + * @option the vertical offset of the legend foreignObject container + * @default 40 + */ legendOffsetY?: number paddingBottom?: number paddingLeft?: number paddingRight?: number paddingTop?: number + /** + * @option standard svg rendering + * @default "auto" + */ "shape-rendering"?: ShapeRendering + /** + * @option show or hide data labels + * @default true + */ showDataLabels?: boolean + /** + * @option show or hide legend + * @default true + */ showLegend?: boolean + /** + * @option show or hide total displayed inside the donut's hollow + * @default true + */ showTotal?: boolean, + /** + * @option the text content of the title element + * @default "" + */ title?: string; + /** + * @option the text color of the title element + * @default "#000000" + */ titleColor?: string + /** + * @option the font size of the title element + * @default 18 + */ titleFontSize?: number + /** + * @option the horizontal position (text-anchor) of the title element + * @default "middle" + */ titlePosition?: TextAnchor + /** + * @option the background color of the tooltip container + * @default "#FFFFFF" + */ tooltipBackgroundColor?: string + /** + * @option the text color of the tooltip content + * @default "#000000" + */ tooltipColor?: string + /** + * @option the text content of the total label inside the donut's hollow + * @default "Total" + */ totalLabel?: string + /** + * @option the text color of the total label inside the donut's hollow + * @default "#000000" + */ totalLabelColor?: string + /** + * @option the font size of the total label inside the donut's hollow + * @default 20 + */ totalLabelFontSize?: number + /** + * @option the font size of the total value inside the donut's hollow + * @default 20 + */ totalValueFontSize?: number + /** + * @option the rounding of the total value inside the donut's hollow + * @default 0 + */ totalValueRounding?: number + /** + * @option the viewBox dimensions of the chart's svg + * @default "0 0 450 450" + */ viewBox?: string } @@ -69,16 +208,18 @@ export function chartDonut({ }) { const globalUid = createUid(); - const grandTotal = dataset.map(ds => ds.value ?? 0).reduce((a, b) => a + b, 0) + const grandTotal = dataset.map(ds => forceNum(ds.value)).reduce((a, b) => a + b, 0) const formattedDataset = dataset.map((ds, i) => { + const value = forceNum(ds.value) return { ...ds, + value, color: ds.color ?? palette[i], "stroke-width": ds["stroke-width"] ?? 20, "stroke-linecap": ds["stroke-linecap"] ?? 'butt', uid: createUid(), - proportion: (ds.value ?? 0) / grandTotal + proportion: value / grandTotal } }).sort((a, b) => b.proportion - a.proportion) diff --git a/savyg/src/utils_chart_gauge.ts b/savyg/src/utils_chart_gauge.ts index c714ebd..989f53d 100644 --- a/savyg/src/utils_chart_gauge.ts +++ b/savyg/src/utils_chart_gauge.ts @@ -1,10 +1,16 @@ import { circle, element, line, path, svg, text } from "."; import { palette } from "./palette"; -import { createUid, getSvgDimensions, makeDonut, fordinum } from "./utils_common"; +import { createUid, getSvgDimensions, makeDonut, fordinum, forceNum } from "./utils_common"; import { ChartArea, DrawingArea, ShapeRendering, SvgItem, TextAnchor } from "./utils_svg_types"; export type ChartGaugeSegment = { + /** + * @description the starting value of a gauge segment + */ from: number + /** + * @escription the ending value of a gauge segment + */ to: number color?: string } @@ -15,37 +21,148 @@ export type ChartGaugeDataset = { } export type ChartGaugeOptions = { + /** + * @option the thickness of the gauge arcs + * @default 58 + */ arcThickness?: number + /** + * @option the background color applied to the chart + * @default "#FFFFFF" + */ backgroundColor?: string + /** + * @option pass any strings separated by a space to generate class names. Example: "my-class1 my-class2" + */ className?: string + /** + * @option the text color of data labels + * @default "#000000" + */ dataLabelsColor?: string + /** + * @option the font size of data labels + * @default 12 + */ dataLabelsFontSize?: number + /** + * @option the data labels offset from the arcs + * @default 1.4 + * @example 1.2 will push data labels closer to the arcs + */ dataLabelsOffset?: number, + /** + * @option font family for all text elements + * @default "inherit" + */ fontFamily?: string + /** + * @option the id of the svg. Defaults to a random uid + */ id?: string paddingBottom?: number paddingLeft?: number paddingRight?: number paddingTop?: number + /** + * @option the color of the pointer base circle + * @default "#1A1A1A" + */ pointerBaseColor?: string + /** + * @option the radius of the pointer base circle + * @default 5 + */ pointerBaseRadius?: number + /** + * @option the border color of the pointer base circle + * @default "#FFFFFF" + */ pointerBaseStroke?: string + /** + * @option the stroke width of the pointer base circle border + * @default 1 + */ pointerBaseStrokeWidth?: number + /** + * @option the color of the pointer + * @default "#2A2A2A" + */ pointerColor?: string + /** + * @option the size of the pointer. 0.9 will make it bigger. Ok this is not logical but whatever + * @default 1 + */ pointerSize?: number + /** + * @option the thickness of the pointer + * @default 5 + */ pointerWidth?: number + /** + * @option standard svg rendering + * @default "auto" + */ "shape-rendering"?: ShapeRendering + /** + * @option show or hide segment data labels + * @default true + */ showDataLabels?: boolean + /** + * @option show or hide the gaueg value + * @default true + */ showValue?: boolean + /** + * @option the text content of the title element + * @default "" + */ title?: string + /** + * @option the text color of the title element + * @default "#000000" + */ titleColor?: string + /** + * @option the font size of the title element + * @default 18 + */ titleFontSize?: number + /** + * @option the font weight of the title element + * @default "bold" + */ titleFontWeight?: "bold" | "normal" + /** + * @option the horizontal position (text-anchor) of the title element + * @default "middle" + */ titlePosition?: TextAnchor + /** + * @option the color of the gauge value + * @default "#000000" + */ valueColor?: string + /** + * @option the font size of the gauge value + * @default 20 + */ valueFontSize?: number + /** + * @option the font weight of the gauge value + * @default "normal" + */ valueFontWeight?: "bold" | "normal" + /** + * @option the rounding of the gauge value + * @default 0 + */ valueRounding?: number + /** + * @option the viewBox dimensions of the chart's svg + * @default "0 0 450 300" + */ viewBox?: string } @@ -130,8 +247,8 @@ export function chartGauge({ const minMax = (function IIFE() { const arr: number[] = [] dataset.segments.forEach(s => { - arr.push(s.from) - arr.push(s.to) + arr.push(forceNum(s.from)) + arr.push(forceNum(s.to)) }) return { max: Math.max(...arr), @@ -142,7 +259,7 @@ export function chartGauge({ const pointerCoordinates = (function IIFE() { const x = drawingArea.centerX const y = drawingArea.centerY + height / 4 - const angle = Math.PI * ((dataset.value - minMax.min) / (minMax.max - minMax.min)) + Math.PI + const angle = Math.PI * ((forceNum(dataset.value) - minMax.min) / (minMax.max - minMax.min)) + Math.PI return { x1: x, y1: y, @@ -158,9 +275,9 @@ export function chartGauge({ ...s, id: `${globalUid}_segment_${i}`, color: s.color ?? palette[i], - value: ((s.to - s.from) / minMax.max) * 100 + value: ((forceNum(s.to) - forceNum(s.from)) / minMax.max) * 100 } - }), { color: 'transparent', value: 0, from: dataset.segments[dataset.segments.length - 1].to, to: dataset.segments[dataset.segments.length - 1].to }] + }), { color: 'transparent', value: 0, from: forceNum(dataset.segments[dataset.segments.length - 1].to), to: forceNum(dataset.segments[dataset.segments.length - 1].to) }] } const segments = element({ diff --git a/savyg/src/utils_chart_xy.ts b/savyg/src/utils_chart_xy.ts index ac331fc..3875e1e 100644 --- a/savyg/src/utils_chart_xy.ts +++ b/savyg/src/utils_chart_xy.ts @@ -1,65 +1,252 @@ import { ChartArea, GradientStop, ShapeRendering, StrokeOptions, SvgItem } from "./utils_svg_types" import { circle, element, line, linearGradient, path, rect, svg, text } from "./utils_svg"; -import { calculateNiceScale, createUid, fordinum, getMaxSerieLength, getMinMaxInDatasetItems, getSvgDimensions, ratioToMax } from "./utils_common"; +import { calculateNiceScale, createUid, forceNum, fordinum, getMaxSerieLength, getMinMaxInDatasetItems, getSvgDimensions, ratioToMax } from "./utils_common"; import { palette } from "./palette"; // TODO: add descriptions for types export type BaseDatasetItem = StrokeOptions & { + /** + * @option data labels vertical offset for a given series + * @default 0 + */ dataLabelOffsetY?: number + /** + * @option data labels color for a given series + * @default "#000000" + */ dataLabelsColor?: string + /** + * @option data labels font size for a given series + */ dataLabelsFontSize?: number fill?: string + /** + * @option the gradient direction for a given bar or area series + * @default "vertical" + */ gradientDirection?: "vertical" | "horizontal", + /** + * @option the gradient starting color for a given bar or area series + */ gradientFrom?: string + /** + * @option the gradient end color for a given bar or area series + */ gradientTo?: string + /** + * @option the name of a given series + */ name?: string + /** + * @option the plot radius for a given line, plot or area series + * @default 1 + */ plotRadius?: number + /** + * @option the rounding of data labels for a given series + * @default 0 + */ rounding?: number + /** + * @option the rx border radius of a given bar series + * @default null + */ rx?: number + /** + * @option the ry border radius of a given bar series + * @default null + */ ry?: number + /** + * @option the graphical type of the series + * @default "line" + */ type?: "line" | "bar" | "area" | "plot" + /** + * @option the values for a given series. null values can be provided in the array + * @example [1, 2, 3, null, 8] + */ values: Array } export type ChartXyDatasetItem = BaseDatasetItem & { + /** + * @option show or hide data labels of a given series + * @default true + */ showDataLabels?: boolean } export type ChartXyOptions = { + /** + * @option the color of x and y axis lines + * @default "#000000" + */ axisColor?: string + /** + * @option the background color applied to the chart + * @default "#FFFFFF" + */ backgroundColor?: string + /** + * @option the spacing between bars in pixels. + * @default 0 + */ barSpacing?: number + /** + * @option pass any strings separated by a space to generate class names. Example: "my-class1 my-class2". + */ className?: string + /** + * @option font family for all text elements. + * @default "inherit" + */ fontFamily?: string + /** + * @option the color of all grid line elements. Can be any color format. + * @default "#CCCCCC" + */ gridColor?: string + /** + * @option the id of the svg. Defaults to a random uid + */ id?: string + /** + * @option activates user interactions (tooltip, zoom) + * @default true + */ interactive?: boolean + /** + * @option the text color of legend elements + * @default "#000000" + */ legendColor?: string + /** + * @option the font size of legend elements + * @default 10 + */ legendFontSize?: number + /** + * @option leave space for the legend + * @default 48 + */ paddingBottom?: number + /** + * @option leave space for y axis labels + * @default 48 + */ paddingLeft?: number + /** + * @option leave space on the right side of the chart + * @default 24 + */ paddingRight?: number + /** + * @option leave space for the title + * @default 48 + */ paddingTop?: number + /** + * @option in interactive mode, color of the selector rect on series hover. Should be a transparent color. + * @default "#00000010" + */ selectorColor?: string + /** + * @option standard svg rendering + * @default "auto" + */ "shape-rendering"?: ShapeRendering + /** + * @option show or hide axis lines + * @default true + */ showAxis?: boolean + /** + * @option show or hide grid lines + * @default true + */ showGrid?: boolean + /** + * @option show or hide legend + * @default true + */ showLegend?: boolean + /** + * @option the text content of the title element + * @default "" + */ title?: string; + /** + * @option the text color of the title element + * @default "#000000" + */ titleColor?: string + /** + * @option the font size of the title element + * @default 18 + */ titleFontSize?: number + /** + * @option the horizontal position (text-anchor) of the title element + * @default "start" + */ titlePosition?: "start" | "middle" | "end" + /** + * @option the background color of the tooltip container + * @default "#FFFFFF" + */ tooltipBackgroundColor?: string + /** + * @option the text color of the tooltip content + * @default "#000000" + */ tooltipColor?: string; + /** + * @option the viewBox dimensions of the chart's svg + * @default "0 0 512 341" + */ viewBox?: string; + /** + * @option the labels on the time axis + * @default [] + * @example ["JAN", "FEB", "MAR", "APR", "MAY", "JUN"] + */ xAxisLabels?: string[] + /** + * @option the color of time labels on the x axis + * @default "#000000" + */ xAxisLabelsColor?: string + /** + * @option the font size of time labels on the x axis + * @default 12 + */ xAxisLabelsFontSize?: number + /** + * @option the vertical offset of time labels on the x axis + * @default 0 + */ xAxisLabelsOffsetY?: number + /** + * @option the color of value labels on the y axis + * @default "#000000" + */ yAxisLabelsColor?: string + /** + * @option the font size of value labels on the y axis + * @default 12 + */ yAxisLabelsFontSize?: number + /** + * @option the rounding of y axis label values + * @default 1 + */ yAxisLabelRounding?: number + /** + * @option the color of the zoom indicator + * @default "#00FF0010" + */ zoomColor?: string } @@ -83,6 +270,7 @@ export function chartXy({ const absoluteDataset = dataset.map((ds, i) => { return { ...ds, + values: ds.values.map(v => forceNum(v)), "stroke-dasharray": ds['stroke-dasharray'] ?? null, "stroke-dashoffset": ds['stroke-dashoffset'] ?? null, "stroke-linecap": ds['stroke-linecap'] ?? 'round', diff --git a/savyg/src/utils_common.ts b/savyg/src/utils_common.ts index 45f3c7a..3d21ecf 100644 --- a/savyg/src/utils_common.ts +++ b/savyg/src/utils_common.ts @@ -14,9 +14,9 @@ export function getSvgDimensions(viewBox: string) { export function getMinMaxInDatasetItems(datasetItems: BaseDatasetItem[], zoom?: { start: number, end: number }) { let flattened; if (zoom) { - flattened = datasetItems.flatMap(d => d.values.filter((_, i) => i >= zoom.start && i <= zoom.end).filter(v => v !== null)) as number[]; + flattened = datasetItems.flatMap(d => d.values.map(v => forceNum(v)).filter((_, i) => i >= zoom.start && i <= zoom.end).filter(v => v !== null)) as number[]; } else { - flattened = datasetItems.flatMap(d => d.values.filter(v => v !== null)) as number[]; + flattened = datasetItems.flatMap(d => d.values.map(v => forceNum(v)).filter(v => v !== null)) as number[]; } return { max: Math.max(...flattened), @@ -27,7 +27,7 @@ export function getMinMaxInDatasetItems(datasetItems: BaseDatasetItem[], zoom?: export function getMaxSerieLength(datasetItems: BaseDatasetItem[], zoom?: { start: number, end: number }) { if (zoom) { return { - maxSeriesLength: Math.max(...datasetItems.map(d => d.values.filter((_v, i) => i >= zoom.start && i <= zoom.end).length)) + maxSeriesLength: Math.max(...datasetItems.map(d => d.values.map(v => forceNum(v)).filter((_v, i) => i >= zoom.start && i <= zoom.end).length)) } } else { return { @@ -267,9 +267,14 @@ export function fordinum(n: number, r: number = 0, s: string = '', p: string = ' return p + (Number(n).toFixed(r)).toLocaleString() + s } +export function forceNum(n: number | string | null | undefined): number { + return isNaN(Number(n)) ? 0 : Number(n) +} + const utils_commons = { calculateNiceScale, createUid, + forceNum, fordinum, getClosestDecimal, getMaxSerieLength, diff --git a/savyg/src/utils_svg.ts b/savyg/src/utils_svg.ts index 2d59530..b27ac35 100644 --- a/savyg/src/utils_svg.ts +++ b/savyg/src/utils_svg.ts @@ -162,6 +162,11 @@ export function use(attrs: { * @returns a svg path element */ export function text(attrs: { + /** + * @option x: number + * @option y: number + * @option content: text element content, cannot be broken into several lines + */ options: SvgOptions[SvgItem.TEXT], parent?: SVGElement | HTMLElement, }) { @@ -455,6 +460,7 @@ export function radialGradient(attrs: { } export function findArcMidpoint(pathElement: SVGPathElement) { + console.log({ pathElement }) const length = pathElement.getTotalLength(); let start = 0; let end = length; diff --git a/savyg/src/utils_svg_types.ts b/savyg/src/utils_svg_types.ts index 56fb614..688d9b1 100644 --- a/savyg/src/utils_svg_types.ts +++ b/savyg/src/utils_svg_types.ts @@ -106,7 +106,13 @@ export type Text = CommonOptions & StrokeOptions & { "font-size"?: number "font-weight"?: "bold" | "normal" "text-anchor"?: TextAnchor + /** + * @description The content of the text element. Should be kept concise, as SVG text element do not break into multiple lines + */ content: string + /** + * @description The color of the text. Can be any color format + */ fill?: string id?: string } diff --git a/savyg/tests/utils_common.test.ts b/savyg/tests/utils_common.test.ts index 396f1a2..4bbaa47 100644 --- a/savyg/tests/utils_common.test.ts +++ b/savyg/tests/utils_common.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest" -import { getSvgDimensions, getMinMaxInDatasetItems, getMaxSerieLength, getClosestDecimal, calculateNiceScale, ratioToMax, fordinum } from "../src/utils_common" +import { getSvgDimensions, getMinMaxInDatasetItems, getMaxSerieLength, getClosestDecimal, calculateNiceScale, ratioToMax, fordinum, forceNum } from "../src/utils_common" describe('getSvgDimensions', () => { test('parses dimensions of a string viewBox', () => { @@ -110,4 +110,15 @@ describe('fordinum', () => { test("returns a number with prefix and suffix", () => { expect(fordinum(1, 0, '_suffix', 'prefix_')).toBe('prefix_1_suffix') }) +}) + +describe('forceNum', () => { + test('forces a number return', () => { + expect(forceNum(1)).toBe(1) + expect(forceNum(1.1)).toBe(1.1) + expect(forceNum("1")).toBe(1) + expect(forceNum("1.1")).toBe(1.1) + expect(forceNum("wut")).toBe(0) + expect(forceNum(null)).toBe(0) + }) }) \ No newline at end of file