diff --git a/.changeset/lovely-hornets-deny.md b/.changeset/lovely-hornets-deny.md new file mode 100644 index 0000000000..cc47781c81 --- /dev/null +++ b/.changeset/lovely-hornets-deny.md @@ -0,0 +1,5 @@ +--- +'@evidence-dev/core-components': minor +--- + +Refactor ReferenceLine and ReferenceArea to use a store, add additional styling props diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.stories.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.stories.svelte new file mode 100644 index 0000000000..4c2a7b3045 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.stories.svelte @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + {@const referenceAreaData = Query.create( + ` + select 30 as xMin, 40 as xMax, 'Area 1' as label union all + select 50, 60, 'Area 2' union all + select 70, 80, 'Area 3' + `, + query + )} + + + + + + + + + {@const referenceAreaData = Query.create( + ` + select 100 as yMin, 150 as yMax, 'Area 1' as label union all + select 850, 1000, 'Area 2' union all + select 200, 400, 'Area 3' + `, + query + )} + + + + + + + + + {@const referenceAreaData = Query.create( + ` + select 30 as xMin, 40 as xMax, 100 as yMin, 150 as yMax, 'Area 1' as label union all + select 50, 60, 850, 1000, 'Area 2' union all + select 70, 80, 200, 400, 'Area 3' + `, + query + )} + + + + + + + + + {@const data = Query.create( + ` + select 'a' as x, 10 as y union all + select 'b', 20 union all + select 'c', 30 + `, + query + )} + + + + + + + + + + + + + + + + + + + + + + {@const referenceAreaData = Query.create( + ` + select 30 as xMin, 40 as xMax, 100 as yMin, 150 as yMax, 'Area 1' as label union all + select 50, 60, 850, 1000, 'Area 2' union all + select 70, 80, 200, 400, 'Area 3' + `, + query + )} + + + + + + diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.svelte index 8afd7553f9..8dce2f0528 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.svelte +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/ReferenceArea.svelte @@ -3,36 +3,173 @@ - - - - - - +{#if $$slots.default} + +{/if} + +{#if $store.error} + +{:else} + + + + +{/if} diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/_ReferenceArea.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/_ReferenceArea.svelte deleted file mode 100644 index 410c7a5631..0000000000 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/_ReferenceArea.svelte +++ /dev/null @@ -1,241 +0,0 @@ - - - - -{#if error} - -{/if} diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/constants.js b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/constants.js new file mode 100644 index 0000000000..cff02de408 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/constants.js @@ -0,0 +1,36 @@ +// @ts-check + +export const COLORS = + /** + * @type {const} + * @satisfies {Record} + */ + ({ + red: { areaColor: '#fceeed', labelColor: '#b04646', borderColor: '#b04646' }, + green: { areaColor: '#e6f5e6', labelColor: '#65a665', borderColor: '#65a665' }, + yellow: { areaColor: '#fff9e0', labelColor: '#edb131', borderColor: '#edb131' }, + grey: { + areaColor: 'hsl(217, 33%, 97%)', + labelColor: 'hsl(212, 10%, 53%)', + borderColor: 'hsl(212, 10%, 53%)' + }, + blue: { areaColor: '#EDF6FD', labelColor: '#51a2e0', borderColor: '#51a2e0' } + }); + +export const LABEL_POSITIONS = + /** + * @type {const} + * @satisfies {Record & Record} + */ + ({ + topLeft: 'insideTopLeft', + top: 'insideTop', + topRight: 'insideTopRight', + bottomLeft: 'insideBottomLeft', + bottom: 'insideBottom', + bottomRight: 'insideBottomRight', + left: 'insideLeft', + center: 'inside', + centre: 'inside', + right: 'insideRight' + }); diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/reference-area.store.js b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/reference-area.store.js new file mode 100644 index 0000000000..8fd4dc1146 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/reference-area.store.js @@ -0,0 +1,249 @@ +// @ts-check + +import { nanoid } from 'nanoid'; +import { get, writable } from 'svelte/store'; +import { COLORS } from './constants.js'; +import { isPresetColor } from '../types.js'; +import checkInputs from '@evidence-dev/component-utilities/checkInputs'; + +/** @template T @typedef {import('svelte/store').Writable} Writable */ +/** @template T @typedef {import('svelte/store').Readable} Readable */ +/** @typedef {import('echarts').EChartsOption} EChartsOption */ +/** @typedef {NonNullable[number][]} MarkAreaData */ +/** @typedef {import('echarts').SeriesOption} SeriesOption */ +/** @typedef {import('echarts').BarSeriesOption} BarSeriesOption */ +/** @typedef {import('echarts').LineSeriesOption} LineSeriesOption */ +/** @typedef {import('./types.js').ReferenceAreaStoreValue} ReferenceAreaStoreValue */ +/** @typedef {import('./types.js').ReferenceAreaConfig} ReferenceAreaConfig */ + +/** @implements {Readable} */ +export class ReferenceAreaStore { + /** @type {Writable} */ + #store = writable({}); + + #id = nanoid(); + + /** @type {Readable} */ + #propsStore; + + /** @type {Writable} */ + #configStore; + + /** + * @param {Readable} propsStore + * @param {Writable} configStore + */ + constructor(propsStore, configStore) { + this.#propsStore = propsStore; + this.#configStore = configStore; + } + + subscribe = this.#store.subscribe; + + /** @param {string | undefined} error */ + setError = (error) => this.#store.update((value) => ({ ...value, error })); + + clearError = () => this.setError(undefined); + + /** @param {ReferenceAreaConfig} config */ + setConfig = (config) => { + this.clearError(); + try { + let { + data, + xMin, + xMax, + yMin, + yMax, + color, + areaColor, + labelColor, + label, + labelPosition, + border, + borderWidth, + borderColor + } = config; + + // TODO maybe we could subscribe to props in here instead of the jank reactive statement in the component + const props = get(this.#propsStore); + if (typeof props === 'undefined') { + throw new Error('Reference Area cannot be used outside of a chart'); + } + + if (props.swapXY) { + [xMin, xMax, yMin, yMax] = [yMin, yMax, xMin, xMax]; + } + + // Default label position based on props + if (typeof labelPosition === 'undefined') { + if (props.swapXY) labelPosition = 'topRight'; + else if (yMin && yMax && xMin && xMax) labelPosition = 'topLeft'; + else if (yMin || yMax) labelPosition = 'right'; + else labelPosition = 'top'; + } + + if (border && typeof borderWidth === 'undefined') { + borderWidth = 1; + } + + // Use preset colors + labelColor = labelColor ?? color; + areaColor = areaColor ?? color; + borderColor = borderColor ?? color; + if (isPresetColor(labelColor)) { + labelColor = COLORS[labelColor].labelColor; + } + if (isPresetColor(areaColor)) { + areaColor = COLORS[areaColor].areaColor; + } + if (isPresetColor(borderColor)) { + borderColor = COLORS[borderColor].borderColor; + } + + /** @type {MarkAreaData} */ + const seriesData = []; + + if (data) { + checkInputs( + data, + [xMin, xMax, yMin, yMax].filter((col) => typeof col !== 'undefined') + ); + + for (let i = 0; i < data.length; i++) { + seriesData.push([ + { + name: label ? (data[i][label] ?? label) : undefined, + xAxis: xMin ? data[i][xMin] : undefined, + yAxis: yMin ? data[i][yMin] : undefined + }, + { + xAxis: xMax ? data[i][xMax] : undefined, + yAxis: yMax ? data[i][yMax] : undefined + } + ]); + } + } else { + seriesData.push([ + { + name: label, + xAxis: xMin, + yAxis: yMin + }, + { + xAxis: xMax, + yAxis: yMax + } + ]); + } + + // Find the series for the bar chart data (if it exists) so we can use the appropriate stack + /** @type {BarSeriesOption | undefined} */ + let barSeries; + const allSeries = get(this.#configStore).series; + if (Array.isArray(allSeries)) { + barSeries = allSeries.find(isBarSeries); + } else if (allSeries) { + barSeries = isBarSeries(allSeries) ? allSeries : undefined; + } + + /** @type {(LineSeriesOption | BarSeriesOption) & {evidenceSeriesType: 'reference_area' }} */ + const series = { + evidenceSeriesType: 'reference_area', + id: this.#id, + type: get(this.#propsStore).chartType === 'Bar Chart' ? 'bar' : 'line', + stack: barSeries?.stack, + animation: false, + silent: true, + markArea: { + data: seriesData, + emphasis: { + disabled: true + }, + itemStyle: { + color: areaColor, + opacity: 1, + borderWidth, + borderColor, + borderType: config.borderType + }, + label: { + show: true, + position: LABEL_POSITIONS[labelPosition], + color: labelColor + } + } + }; + + this.#configStore.update((config) => { + if (!config.series) config.series = []; + if (!Array.isArray(config.series)) config.series = [config.series]; + + const existingDataIndex = config.series.findIndex((series) => series.id === this.#id); + if (existingDataIndex === -1) { + config.series.push(series); + } else { + config.series[existingDataIndex] = series; + } + + // Make sure area aligns with categorical axis on bar charts correctly + if (props.swapXY) { + if (Array.isArray(config.yAxis)) { + config.yAxis.forEach((yAxis) => { + if (yAxis.type === 'category') { + yAxis.axisTick = { + ...yAxis.axisTick, + alignWithLabel: false + }; + } + }); + } else if (config.yAxis) { + config.yAxis.axisTick = { + ...config.yAxis.axisTick, + alignWithLabel: false + }; + } + } else { + if (Array.isArray(config.xAxis)) { + config.xAxis.forEach((xAxis) => { + if (xAxis.type === 'category') { + xAxis.axisTick = { + ...xAxis.axisTick, + alignWithLabel: false + }; + } + }); + } else if (config.xAxis) { + config.xAxis.axisTick = { + ...config.xAxis.axisTick, + alignWithLabel: false + }; + } + } + + return config; + }); + } catch (e) { + this.setError(String(/** @type {any} */ (e).message)); + } + }; +} + +export const LABEL_POSITIONS = /** @type {const} */ ({ + topLeft: 'insideTopLeft', + top: 'insideTop', + topRight: 'insideTopRight', + bottomLeft: 'insideBottomLeft', + bottom: 'insideBottom', + bottomRight: 'insideBottomRight', + left: 'insideLeft', + center: 'inside', + centre: 'inside', + right: 'insideRight' +}); + +/** + * @param {SeriesOption} series + * @returns {series is BarSeriesOption} + */ +const isBarSeries = (series) => series.type === 'bar' && !('evidenceSeriesType' in series); diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/types.d.ts b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/types.d.ts new file mode 100644 index 0000000000..0bafe66250 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceArea/types.d.ts @@ -0,0 +1,57 @@ +// @ts-check + +import { Writable, Readable } from 'svelte/store'; +import type { MarkAreaComponentOption } from 'echarts'; + +import type { Color } from '../colors.js'; +import type { Symbol } from '../references.d.ts'; + +export type LabelPosition = + | 'topLeft' + | 'top' + | 'topRight' + | 'bottomLeft' + | 'bottom' + | 'bottomRight' + | 'left' + | 'center' + | 'right'; + +export type ReferenceAreaConfig = { + // Data + data?: any; + xMin?: number | string; + xMax?: number | string; + yMin?: number | string; + yMax?: number | string; + label?: string; + + // Color + color?: Color; + + // Area styling + areaColor?: Color; + opacity?: number; + border?: boolean; + borderType?: 'solid' | 'dotted' | 'dashed'; + borderColor?: Color; + borderWidth?: number; + + // Label styling + labelColor?: Color; + labelPadding?: number; + labelPosition?: LabelPosition; + labelBackgroundColor?: string; + labelBorderColor?: string; + labelBorderWidth?: number; + labelBorderRadius?: number; + labelBorderType?: 'solid' | 'dotted' | 'dashed'; + fontSize?: number; + align?: 'left' | 'center' | 'right'; + bold?: boolean; + italic?: boolean; +}; + +export type ReferenceAreaStoreValue = { + error?: string; +}; diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.stories.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.stories.svelte index dad78764b0..610c81f6ff 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.stories.svelte +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.stories.svelte @@ -2,37 +2,322 @@ /** @type {import('@storybook/addon-svelte-csf').MetaProps}*/ export const meta = { title: 'viz/references/ReferenceLine', - component: ReferenceLine + component: ReferenceLine, + argTypes: { + emptySet: { + control: 'select', + options: ['pass', 'warn', 'error'] + }, + emptyMessage: { + control: 'text' + }, + label: { + control: 'text' + }, + color: { + control: 'color' + }, + symbol: { + control: 'select', + options: ['circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'] + }, + symbolSize: { + control: 'number' + }, + symbolStart: { + control: 'select', + options: ['circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'] + }, + symbolStartSize: { + control: 'number' + }, + symbolEnd: { + control: 'select', + options: ['circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'] + }, + symbolEndSize: { + control: 'number' + }, + lineType: { + control: 'select', + options: ['solid', 'dotted', 'dashed'] + }, + lineColor: { + control: 'color' + }, + lineWidth: { + control: 'number' + }, + hideValue: { + control: 'boolean' + }, + labelColor: { + control: 'color' + }, + labelPadding: { + control: 'number' + }, + labelPosition: { + control: 'select', + options: [ + 'left', + 'right', + 'top', + 'bottom', + 'inside', + 'insideLeft', + 'insideRight', + 'insideTop', + 'insideBottom', + 'insideTopLeft', + 'insideTopRight', + 'insideBottomLeft', + 'insideBottomRight' + ] + }, + labelBackgroundColor: { + control: 'color' + }, + labelBorderWidth: { + control: 'number' + }, + labelBorderRadius: { + control: 'number' + }, + labelBorderColor: { + control: 'color' + }, + labelBorderType: { + control: 'select', + options: ['solid', 'dotted', 'dashed'] + }, + fontSize: { + control: 'number' + }, + align: { + control: 'select', + options: ['left', 'center', 'right'] + }, + bold: { + control: 'boolean' + }, + italic: { + control: 'boolean' + }, + preserveWhitespace: { + control: 'boolean' + } + }, + args: { + label: 'Reference Line' + } }; - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 30 as x, 'Line 1' as label union all + select 50, 'Line 2' union all + select 70, 'Line 3' + `, + query + )} + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 30 as x, 'Line 1' as label union all + select 50, 'Line 2' union all + select 70, 'Line 3' + `, + query + )} + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 300 as y, 'Line 1' as label union all + select 500, 'Line 2' union all + select 700, 'Line 3' + `, + query + )} + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 300 as y, 'Line 1' as label union all + select 500, 'Line 2' union all + select 700, 'Line 3' + `, + query + )} + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 30 as x, 300 as y, 40 as x2, 400 as y2, 'Line 1' as label union all + select 50, 500, 60, 400, 'Line 2' union all + select 80, 800, 70, 1, 'Line 3' union all + select 20, 400, 10, 1000, 'Line 4' + `, + query + )} + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 30 as x, 300 as y, 40 as x2, 400 as y2, 'Line 1' as label union all + select 50, 500, 60, 400, 'Line 2' union all + select 80, 800, 70, 1, 'Line 3' union all + select 20, 400, 10, 1000, 'Line 4' + `, + query + )} + + + + + + + + { // Reference line should move when X value is updated await data.fetch(); @@ -48,3 +333,48 @@ + + + {@const data = Query.create( + ` + select 'a' as x, 10 as y union all + select 'b', 20 union all + select 'c', 30 + `, + query + )} + + + + + + + + + + + + + + + + + + + + + + {@const referenceLineData = Query.create( + ` + select 30 as x, 'Line 1' as label union all + select 50, 'Line 2' union all + select 70, 'Line 3' + `, + query + )} + + + + + + diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.svelte index a006ec07e4..e2c69c085a 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.svelte +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/ReferenceLine.svelte @@ -3,36 +3,215 @@ - - - - - - +{#if $$slots.default} + +{/if} + +{#if $store.error} + +{:else} + + + + +{/if} diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/_ReferenceLine.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/_ReferenceLine.svelte deleted file mode 100644 index 09f270cab2..0000000000 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/_ReferenceLine.svelte +++ /dev/null @@ -1,271 +0,0 @@ - - - - -{#if error} - -{/if} diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/constants.js b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/constants.js new file mode 100644 index 0000000000..c6b07adc89 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/constants.js @@ -0,0 +1,48 @@ +// @ts-check + +import { uiColours } from '@evidence-dev/component-utilities/colours'; + +export const COLORS = + /** + * @type {const} + * @satisfies {Record} + */ + ({ + red: { lineColor: '#b04646', labelColor: '#b04646', symbolColor: '#b04646' }, + green: { + lineColor: uiColours.green700, + labelColor: uiColours.green700, + symbolColor: uiColours.green700 + }, + yellow: { + lineColor: uiColours.yellow600, + labelColor: uiColours.yellow700, + symbolColor: uiColours.yellow600 + }, + grey: { + lineColor: uiColours.grey500, + labelColor: uiColours.grey600, + symbolColor: uiColours.grey500 + }, + blue: { + lineColor: uiColours.blue500, + labelColor: uiColours.blue500, + symbolColor: uiColours.blue500 + } + }); + +export const LABEL_POSITIONS = + /** + * @type {const} + * @satisfies {Record & Record} + */ + ({ + aboveEnd: 'insideEndTop', + aboveStart: 'insideStartTop', + aboveCenter: 'insideMiddleTop', + aboveCentre: 'insideMiddleTop', + belowEnd: 'insideEndBottom', + belowStart: 'insideStartBottom', + belowCenter: 'insideMiddleBottom', + belowCentre: 'insideMiddleBottom' + }); diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/reference-line.store.js b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/reference-line.store.js new file mode 100644 index 0000000000..ff450c1f13 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/reference-line.store.js @@ -0,0 +1,237 @@ +// @ts-check + +import { nanoid } from 'nanoid'; +import { get, writable } from 'svelte/store'; +import checkInputs from '@evidence-dev/component-utilities/checkInputs'; +import { formatValue } from '@evidence-dev/component-utilities/formatting'; +import { isPresetColor } from '../types.js'; +import { COLORS, LABEL_POSITIONS } from './constants.js'; + +/** @template T @typedef {import('svelte/store').Writable} Writable */ +/** @template T @typedef {import('svelte/store').Readable} Readable */ +/** @typedef {import('echarts').EChartsOption} EChartsOption */ +/** @typedef {NonNullable[number][]} MarkLineData */ +/** @typedef {import('echarts').LineSeriesOption} LineSeriesOption */ +/** @typedef {import('./types.js').ReferenceLineStoreValue} ReferenceLineStoreValue */ +/** @typedef {import('./types.js').ReferenceLineConfig} ReferenceLineConfig */ + +/** @implements {Readable} */ +export class ReferenceLineStore { + /** @type {Writable} */ + #store = writable({}); + + #id = nanoid(); + + /** @type {Readable} */ + #propsStore; + + /** @type {Writable} */ + #configStore; + + /** + * @param {Readable} propsStore + * @param {Writable} configStore + */ + constructor(propsStore, configStore) { + this.#propsStore = propsStore; + this.#configStore = configStore; + } + + subscribe = this.#store.subscribe; + + /** @param {string | undefined} error */ + setError = (error) => this.#store.update((value) => ({ ...value, error })); + + clearError = () => this.setError(undefined); + + /** @param {ReferenceLineConfig} config */ + setConfig = (config) => { + this.clearError(); + try { + let { data, x, y, x2, y2, color, labelColor, lineColor, label, hideValue } = config; + + // TODO maybe we could subscribe to this in here instead of the jank reactive statement in the component + const props = get(this.#propsStore); + if (typeof props === 'undefined') { + throw new Error('Reference Line cannot be used outside of a chart'); + } + let { xFormat, yFormat, swapXY } = props; + + if (swapXY) { + [x, y] = [y, x]; + [x2, y2] = [y2, x2]; + [xFormat, yFormat] = [yFormat, xFormat]; + } + + const symbolStartConfig = { + symbol: config.symbolStart, + symbolSize: config.symbolStartSize, + symbolKeepAspect: true + }; + + const symbolEndConfig = { + symbol: config.symbolEnd, + symbolSize: config.symbolEndSize, + symbolKeepAspect: true + }; + + [symbolStartConfig, symbolEndConfig].forEach((symbolConfig) => { + if (symbolConfig.symbol === 'arrow') { + // Use a nicer arrow symbol + symbolConfig.symbol = 'path://M0,10 L5,0 L10,10 z'; + } else if (symbolConfig.symbol === 'none') { + // using symbol=none removes the label, which we dont want + // so we set symbolSize=0 instead + symbolConfig.symbol = undefined; + symbolConfig.symbolSize = 0; + } + }); + + // Use preset colors + labelColor = labelColor ?? color; + lineColor = lineColor ?? color; + if (isPresetColor(labelColor)) { + labelColor = COLORS[labelColor].labelColor; + } + if (isPresetColor(lineColor)) { + lineColor = COLORS[lineColor].lineColor; + } + + const labelPosition = config.labelPosition + ? LABEL_POSITIONS[config.labelPosition] + : 'insideEndTop'; + + /** @type {MarkLineData} */ + const seriesData = []; + + if (typeof data !== 'undefined' && data[Symbol.iterator]) { + checkInputs( + data, + [x, y, x2, y2].filter((col) => typeof col !== 'undefined') + ); + } + + if (typeof x !== 'undefined' && typeof y !== 'undefined') { + if (typeof x2 === 'undefined' && typeof y2 === 'undefined') { + throw new Error('Either x2 or y2 must be provided when x and y are provided'); + } + + if (typeof data !== 'undefined' && data[Symbol.iterator]) { + for (let i = 0; i < data.length; i++) { + const _x1 = data[i][x]; + const _y1 = data[i][y]; + const _x2 = data[i][x2 || x]; + const _y2 = data[i][y2 || y]; + const name = label ? (data[i][label] ?? label) : undefined; + seriesData.push([ + { coord: [_x1, _y1], name, ...symbolStartConfig }, + { coord: [_x2, _y2] } + ]); + } + } else { + const _x2 = x2 || x; + const _y2 = y2 || y; + const name = label; + seriesData.push([{ coord: [x, y], name, ...symbolStartConfig }, { coord: [_x2, _y2] }]); + } + } else if (typeof x !== 'undefined') { + if (typeof data !== 'undefined' && data[Symbol.iterator]) { + for (let i = 0; i < data.length; i++) { + const _x = data[i][x]; + const name = label ? (data[i][label] ?? label) : undefined; + seriesData.push({ xAxis: _x, name, ...symbolStartConfig }); + } + } else { + const name = label; + seriesData.push({ xAxis: x, name, ...symbolStartConfig }); + } + } else if (typeof y !== 'undefined') { + if (typeof data !== 'undefined' && data[Symbol.iterator]) { + for (let i = 0; i < data.length; i++) { + const _y = data[i][y]; + const name = label ? (data[i][label] ?? label) : undefined; + seriesData.push({ yAxis: _y, name, ...symbolStartConfig }); + } + } else { + const name = label; + seriesData.push({ yAxis: y, name, ...symbolStartConfig }); + } + } else { + throw new Error('Either x or y must be provided when data is provided'); + } + + /** @satisfies {LineSeriesOption & {evidenceSeriesType: 'reference_line' }} */ + const series = { + evidenceSeriesType: 'reference_line', + id: this.#id, + type: 'line', + animation: false, + silent: true, + markLine: { + data: seriesData, + animation: false, + ...symbolEndConfig, + emphasis: { + disabled: true + }, + label: { + show: true, + position: labelPosition, + color: labelColor, + backgroundColor: config.labelBackgroundColor, + padding: config.labelPadding, + borderRadius: config.labelBorderRadius, + formatter: (params) => { + const label = params.name; + let result = ''; + + const { xAxis, yAxis } = /** @type {Record} */ ( + params.data + ); + + const hasY = typeof y !== 'undefined'; + const hasX = typeof x !== 'undefined'; + const isSloped = hasY && hasX; + + const value = formatValue( + hasY ? yAxis : hasX ? xAxis : params.value, + hasY ? yFormat : hasX ? xFormat : 'string' + ); + + if (label) { + result += label; + if (!hideValue && !isSloped) { + result += ` (${value})`; + } + } else if (!hideValue) { + result += value; + } + + return result; + } + }, + lineStyle: { + color: lineColor, + width: config.lineWidth, + type: config.lineType + } + } + }; + + this.#configStore.update((config) => { + if (!config.series) config.series = []; + if (!Array.isArray(config.series)) config.series = [config.series]; + + const existingDataIndex = config.series.findIndex((series) => series.id === this.#id); + if (existingDataIndex === -1) { + config.series.push(series); + } else { + config.series[existingDataIndex] = series; + } + return config; + }); + } catch (e) { + this.setError(String(/** @type {any} */ (e).message)); + } + }; +} diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/types.d.ts b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/types.d.ts new file mode 100644 index 0000000000..1606b5e94f --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferenceLine/types.d.ts @@ -0,0 +1,57 @@ +// @ts-check + +import { Writable, Readable } from 'svelte/store'; + +import type { ReferenceColor, Symbol } from '../types.js'; +import type { MarkLineComponentOption } from 'echarts'; + +export type LabelPosition = + | 'aboveStart' + | 'aboveCenter' + | 'aboveEnd' + | 'belowStart' + | 'belowCenter' + | 'belowEnd'; + +export type ReferenceLineConfig = { + // Data + data?: any; + x?: number | string; + y?: number | string; + x2?: number | string; + y2?: number | string; + label?: string; + + // Color + color?: ReferenceColor; + + // Symbol styling + symbolStart?: Symbol; + symbolStartSize?: number; + symbolEnd?: Symbol; + symbolEndSize?: number; + + // Line styling + lineType?: 'solid' | 'dotted' | 'dashed'; + lineColor?: ReferenceColor; + lineWidth?: number; + + // Label styling + labelColor?: ReferenceColor; + labelPadding?: number; + labelPosition?: LabelPosition; + labelBackgroundColor?: string; + labelBorderColor?: string; + labelBorderWidth?: number; + labelBorderRadius?: number; + labelBorderType?: 'solid' | 'dotted' | 'dashed'; + hideValue?: boolean; + fontSize?: number; + align?: 'left' | 'center' | 'right'; + bold?: boolean; + italic?: boolean; +}; + +export type ReferenceLineStoreValue = { + error?: string; +}; diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/Callout.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/Callout.svelte index c5566c4766..9c2e2a0f98 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/Callout.svelte +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/Callout.svelte @@ -24,7 +24,7 @@ export let data = undefined; /** - * @type {import('../colors.js').Color} + * @type {import('../types.js').ReferenceColor} * @default "grey" */ export let color = 'grey'; @@ -32,7 +32,7 @@ /** @type {string | undefined} */ export let label = undefined; - /** @type {import('../colors.js').Color | undefined} */ + /** @type {import('../types.js').ReferenceColor | undefined} */ export let labelColor = undefined; /** @@ -48,7 +48,7 @@ export let labelPadding = 5; /** - * @type {import('./reference-point.d.ts').LabelPosition} + * @type {import('./types.js').LabelPosition} * @default "top" */ export let labelPosition = 'top'; @@ -93,12 +93,12 @@ export let italic = undefined; /** - * @type {import('./reference-point.d.ts').Symbol} + * @type {import('../types.js').Symbol} * @default "circle" */ export let symbol = 'circle'; - /** @type {import('../colors.js').Color | undefined} */ + /** @type {import('../types.js').ReferenceColor | undefined} */ export let symbolColor = undefined; /** diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.stories.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.stories.svelte index 99b8befcd1..e8ea9197e0 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.stories.svelte +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.stories.svelte @@ -107,6 +107,7 @@ import { query } from '@evidence-dev/universal-sql/client-duckdb'; import { INPUTS_CONTEXT_KEY } from '@evidence-dev/component-utilities/globalContexts'; import LineChart from '$lib/unsorted/viz/line/LineChart.svelte'; + import BarChart from '$lib/unsorted/viz//bar/BarChart.svelte'; import QueryLoad from '../../../../atoms/query-load/QueryLoad.svelte'; import ReferencePoint from './ReferencePoint.svelte'; @@ -170,6 +171,32 @@ + + {@const data = Query.create( + ` + select 'a' as x, 10 as y union all + select 'b', 20 union all + select 'c', 30 + `, + query + )} + + + + + + + {@const data = Query.create(`SELECT * FROM numeric_series WHERE series='pink'`, query)} + + + + + + + + + + {@const data = Query.create(`SELECT * FROM numeric_series WHERE series='pink'`, query)} @@ -212,3 +239,37 @@ + + + + + + + {@const chartData = Query.create(`SELECT * FROM numeric_series WHERE series='pink'`, query)} + {@const referencePointData = Query.create( + ` + SELECT + x, + y, + row_number() over(order by x) as label + FROM numeric_series + WHERE + series='pink' AND + x in (30, 50, 70) + `, + query + )} + + + + + + + + + diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.svelte b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.svelte index 268694d959..01440195bb 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.svelte +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/ReferencePoint.svelte @@ -11,7 +11,7 @@ import EmptyChart from '../../core/EmptyChart.svelte'; import ErrorChart from '../../core/ErrorChart.svelte'; import { Query } from '@evidence-dev/sdk/usql'; - import { createReferencePointStore } from './reference-point.store.js'; + import { ReferencePointStore } from './reference-point.store.js'; import { toNumber } from '../../../../utils.js'; /** @type {'pass' | 'warn' | 'error' | undefined} */ @@ -30,7 +30,7 @@ export let data = undefined; /** - * @type {import('../colors.js').Color} + * @type {import('../types.js').ReferenceColor} * @default "grey" */ export let color = 'grey'; @@ -38,7 +38,7 @@ /** @type {string | undefined} */ export let label = undefined; - /** @type {import('../colors.js').Color | undefined} */ + /** @type {import('../types.js').ReferenceColor | undefined} */ export let labelColor = undefined; /** @@ -46,14 +46,12 @@ * @default "fit" */ export let labelWidth = 'fit'; - $: labelWidth = labelWidth === 'fit' ? undefined : toNumber(labelWidth); /** @type {number | string | undefined} */ export let labelPadding = undefined; - $: labelPadding = toNumber(labelPadding); /** - * @type {import('./reference-point.d.ts').LabelPosition} + * @type {import('./types.js').LabelPosition} * @default "top" */ export let labelPosition = 'top'; @@ -66,11 +64,9 @@ /** @type {number | string | undefined} */ export let labelBorderWidth = undefined; - $: labelBorderWidth = toNumber(labelBorderWidth); /** @type {number | string | undefined} */ export let labelBorderRadius = undefined; - $: labelBorderRadius = toNumber(labelBorderRadius); /** @type {string | undefined} */ export let labelBorderColor = undefined; @@ -80,7 +76,6 @@ /** @type {number | string | undefined} */ export let fontSize = undefined; - $: fontSize = toNumber(fontSize); /** @type {'left' | 'center' | 'right' | undefined} */ export let align = undefined; @@ -92,12 +87,12 @@ export let italic = undefined; /** - * @type {import('./reference-point.d.ts').Symbol} + * @type {import('../types.js').Symbol} * @default "circle" */ export let symbol = 'circle'; - /** @type {import('../colors.js').Color | undefined} */ + /** @type {import('../types.js').ReferenceColor | undefined} */ export let symbolColor = undefined; /** @@ -105,15 +100,12 @@ * @default 8 */ export let symbolSize = 8; - $: symbolSize = toNumber(symbolSize) ?? 0; /** @type {number | string | undefined} */ export let symbolOpacity = undefined; - $: symbolOpacity = toNumber(symbolOpacity); /** @type {number | string | undefined} */ export let symbolBorderWidth = undefined; - $: symbolBorderWidth = toNumber(symbolBorderWidth); /** @type {string | undefined} */ export let symbolBorderColor = undefined; @@ -147,11 +139,11 @@ const props = getPropContext(); const config = getConfigContext(); - const store = createReferencePointStore(config); + const store = new ReferencePointStore(props, config); // React to the props store to make sure the ReferencePoint is added after the chart is fully rendered $: $props, - ($store = { + store.setConfig({ data, x, y, @@ -160,19 +152,19 @@ color, labelColor, symbolColor, - symbolSize, - symbolOpacity, - symbolBorderWidth, + symbolSize: toNumber(symbolSize), + symbolOpacity: toNumber(symbolOpacity), + symbolBorderWidth: toNumber(symbolBorderWidth), symbolBorderColor, - labelWidth, - labelPadding, + labelWidth: labelWidth === 'fit' ? undefined : toNumber(labelWidth), + labelPadding: toNumber(labelPadding), labelPosition, labelBackgroundColor, - labelBorderWidth, - labelBorderRadius, + labelBorderWidth: toNumber(labelBorderWidth), + labelBorderRadius: toNumber(labelBorderRadius), labelBorderColor, labelBorderType, - fontSize, + fontSize: toNumber(fontSize), align, bold, italic diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/constants.js b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/constants.js new file mode 100644 index 0000000000..9bb8acb4a3 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/constants.js @@ -0,0 +1,16 @@ +// @ts-check + +import { uiColours } from '@evidence-dev/component-utilities/colours'; + +export const COLORS = + /** + * @type {const} + * @satisfies {Record} + */ + ({ + red: { symbolColor: '#b04646', labelColor: '#b04646' }, + green: { symbolColor: uiColours.green700, labelColor: uiColours.green700 }, + yellow: { symbolColor: uiColours.yellow600, labelColor: uiColours.yellow700 }, + grey: { symbolColor: uiColours.grey500, labelColor: uiColours.grey600 }, + blue: { symbolColor: uiColours.blue500, labelColor: uiColours.blue500 } + }); diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.store.js b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.store.js index 0ac0772076..eb91fe994e 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.store.js +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.store.js @@ -2,168 +2,195 @@ import { get, writable } from 'svelte/store'; import { nanoid } from 'nanoid'; -import { getLineAndSymbolColors } from '../colors.js'; import checkInputs from '@evidence-dev/component-utilities/checkInputs'; +import { isPresetColor } from '../types.js'; +import { COLORS } from './constants.js'; -/** - * @param {import('svelte/store').Writable} configStore - * @returns {import('./reference-point.d.ts').ReferencePointStore} - */ -export const createReferencePointStore = (configStore) => { - /** @type {import('./reference-point.d.ts').ReferencePointStore} */ - const store = writable({}); +/** @template T @typedef {import('svelte/store').Writable} Writable */ +/** @template T @typedef {import('svelte/store').Readable} Readable */ +/** @typedef {import('echarts').EChartsOption} EChartsOption */ +/** @typedef {NonNullable[number][]} MarkPointData */ +/** @typedef {import('echarts').LineSeriesOption} LineSeriesOption */ +/** @typedef {import('./types.js').ReferencePointStoreValue} ReferencePointStoreValue */ +/** @typedef {import('./types.js').ReferencePointConfig} ReferencePointConfig */ + +/** @implements {Readable} */ +export class ReferencePointStore { + /** @type {Writable} */ + #store = writable({}); + + #id = nanoid(); + + /** @type {Readable} */ + #propsStore; + + /** @type {Writable} */ + #configStore; + + /** + * @param {Readable} propsStore + * @param {Writable} configStore + */ + constructor(propsStore, configStore) { + this.#propsStore = propsStore; + this.#configStore = configStore; + } + + subscribe = this.#store.subscribe; /** @param {string | undefined} error */ - const setError = (error) => store.update((state) => ({ ...state, error })); - const clearError = () => setError(undefined); - - const id = nanoid(); - - /** @param {import('./reference-point.d.ts').ReferencePointStoreState} state */ - const updateChartConfig = (state) => { - const { labelColor, symbolColor } = getLineAndSymbolColors(state); - - // Destructure some properties for QOL preprocessing - let { - symbolSize, - label, - labelPosition, - labelBorderWidth, - labelBorderColor, - symbolBorderWidth, - symbolBorderColor, - align - } = state; - - /** @type {import('./reference-point.d.ts').Symbol | string | undefined} */ - let symbol = state.symbol; - if (symbol === 'arrow') { - // Use a nicer arrow symbol - symbol = 'path://M0,10 L5,0 L10,10 z'; - } else if (symbol === 'none') { - // using symbol=none removes the label, which we dont want - // so we set symbolSize=0 instead - symbol = undefined; - symbolSize = 0; - } + setError = (error) => this.#store.update((value) => ({ ...value, error })); - // Default labelBorderWidth and labelBorderColor if only one is given - if (labelBorderColor && typeof labelBorderWidth === 'undefined') { - labelBorderWidth = 1; - } else if (labelBorderWidth && !labelBorderColor) { - labelBorderColor = 'gray'; - } + clearError = () => this.setError(undefined); - // Default symbolBorderWidth and symbolBorderColor if only one is given - if (symbolBorderColor && typeof symbolBorderWidth === 'undefined') { - symbolBorderWidth = 1; - } else if (symbolBorderWidth && !symbolBorderColor) { - symbolBorderColor = 'gray'; - } + /** @param {ReferencePointConfig} config */ + setConfig = (config) => { + this.clearError(); + try { + // Destructure some properties for QOL preprocessing + let { + data, + x, + y, + color, + symbol, + symbolSize, + symbolColor, + label, + labelColor, + labelPosition, + labelBorderWidth, + labelBorderColor, + symbolBorderWidth, + symbolBorderColor, + align + } = config; + + const props = get(this.#propsStore); + if (typeof props === 'undefined') { + throw new Error('Reference Point cannot be used outside of a chart'); + } + + if (props.swapXY) { + [x, y] = [y, x]; + } + + if (symbol === 'arrow') { + // Use a nicer arrow symbol + symbol = 'path://M0,10 L5,0 L10,10 z'; + } else if (symbol === 'none') { + // using symbol=none removes the label, which we dont want + // so we set symbolSize=0 instead + symbol = undefined; + symbolSize = 0; + } - /** @type {Partial[number]>} */ - const seriesDataCommon = { - symbol, - symbolSize, - symbolKeepAspect: true, - itemStyle: { - color: symbolColor, - opacity: state.symbolOpacity, - borderWidth: state.symbolBorderWidth, - borderColor: state.symbolBorderColor + // Use preset colors + labelColor = labelColor ?? color; + symbolColor = symbolColor ?? color; + if (isPresetColor(color)) { + if (!labelColor) labelColor = COLORS[color].labelColor; + if (!symbolColor) symbolColor = COLORS[color].symbolColor; } - }; - - const { data, x, y } = state; - /** @type {NonNullable[number][]} */ - let seriesData = []; - if (typeof x !== 'undefined' && typeof y !== 'undefined') { - if (typeof data !== 'undefined' && data[Symbol.iterator]) { - checkInputs(data, [x, y]); - for (let i = 0; i < data.length; i++) { + + // Default labelBorderWidth and labelBorderColor if only one is given + if (labelBorderColor && typeof labelBorderWidth === 'undefined') { + labelBorderWidth = 1; + } else if (labelBorderWidth && !labelBorderColor) { + labelBorderColor = 'gray'; + } + + // Default symbolBorderWidth and symbolBorderColor if only one is given + if (symbolBorderColor && typeof symbolBorderWidth === 'undefined') { + symbolBorderWidth = 1; + } else if (symbolBorderWidth && !symbolBorderColor) { + symbolBorderColor = 'gray'; + } + + /** @type {Partial} */ + const seriesDataCommon = { + symbol, + symbolSize, + symbolKeepAspect: true, + itemStyle: { + color: symbolColor, + opacity: config.symbolOpacity, + borderWidth: config.symbolBorderWidth, + borderColor: config.symbolBorderColor + } + }; + + /** @type {MarkPointData} */ + let seriesData = []; + if (typeof x !== 'undefined' && typeof y !== 'undefined') { + if (typeof data !== 'undefined' && data[Symbol.iterator]) { + checkInputs(data, [x, y]); + for (let i = 0; i < data.length; i++) { + seriesData.push({ + ...seriesDataCommon, + coord: [data[i][x], data[i][y]], + name: (label ? data[i][label] : undefined) ?? label, + value: (label ? data[i][label] : undefined) ?? label + }); + } + } else { seriesData.push({ ...seriesDataCommon, - coord: [data[i][x], data[i][y]], - name: (label ? data[i][label] : undefined) ?? label, - value: (label ? data[i][label] : undefined) ?? label + coord: [x, y], + name: label ?? this.#id, + value: label }); } } else { - seriesData.push({ - ...seriesDataCommon, - coord: [x, y], - name: label ?? id, - value: label - }); + throw new Error('You must provide x and y'); } - } else { - throw new Error('You must provide x and y'); - } - /** @type {import('echarts').LineSeriesOption & { evidenceSeriesType: 'reference_point' }} */ - const series = { - evidenceSeriesType: 'reference_point', - id, - type: 'line', - animation: false, - silent: true, - markPoint: { - data: seriesData, - label: { - width: state.labelWidth, - padding: state.labelPadding, - position: labelPosition, - color: labelColor, - opacity: 1, - backgroundColor: state.labelBackgroundColor, - borderColor: state.labelBorderColor, - borderWidth: state.labelBorderWidth, - borderRadius: state.labelBorderRadius, - borderType: state.labelBorderType, - overflow: 'break', - fontSize: state.fontSize, - align, - fontWeight: state.bold ? 'bold' : undefined, - fontStyle: state.italic ? 'italic' : undefined - }, - emphasis: { - disabled: true + /** @type {import('echarts').LineSeriesOption & { evidenceSeriesType: 'reference_point' }} */ + const series = { + evidenceSeriesType: 'reference_point', + id: this.#id, + type: 'line', + animation: false, + silent: true, + markPoint: { + data: seriesData, + label: { + width: config.labelWidth, + padding: config.labelPadding, + position: labelPosition, + color: labelColor, + opacity: 1, + backgroundColor: config.labelBackgroundColor, + borderColor: config.labelBorderColor, + borderWidth: config.labelBorderWidth, + borderRadius: config.labelBorderRadius, + borderType: config.labelBorderType, + overflow: 'break', + fontSize: config.fontSize, + align, + fontWeight: config.bold ? 'bold' : undefined, + fontStyle: config.italic ? 'italic' : undefined + }, + emphasis: { + disabled: true + } } - } - }; - - configStore.update((config) => { - const existingDataIndex = config.series.findIndex( - (/** @type {{ id?: string; }} */ series) => series.id === id - ); - if (existingDataIndex === -1) { - config.series.push(series); - } else { - config.series[existingDataIndex] = series; - } - return config; - }); - }; + }; - return { - subscribe: store.subscribe, - set: (state) => { - clearError(); - try { - updateChartConfig(state); - } catch (e) { - setError(String(e)); - } - }, - update: (cb) => { - clearError(); - let state = get(store); - try { - state = cb(state); - updateChartConfig(state); - } catch (e) { - setError(String(e)); - } + this.#configStore.update((config) => { + if (!config.series) config.series = []; + if (!Array.isArray(config.series)) config.series = [config.series]; + + const existingDataIndex = config.series.findIndex((series) => series.id === this.#id); + if (existingDataIndex === -1) { + config.series.push(series); + } else { + config.series[existingDataIndex] = series; + } + return config; + }); + } catch (e) { + this.setError(String(/** @type {any} */ (e).message)); } }; -}; +} diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.d.ts b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/types.d.ts similarity index 62% rename from packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.d.ts rename to packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/types.d.ts index 672f956cb4..fce8e59788 100644 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/reference-point.d.ts +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/ReferencePoint/types.d.ts @@ -2,29 +2,20 @@ import type { Writable, Readable } from 'svelte/store'; import type { MarkPointComponentOption } from 'echarts'; -import type { Color } from '../colors.js'; -export type LabelPosition = MarkPointComponentOption['label']['position']; +import type { ReferenceColor, Symbol } from '../types.js'; -export type Symbol = - | 'circle' - | 'rect' - | 'roundRect' - | 'triangle' - | 'diamond' - | 'pin' - | 'arrow' - | 'none'; +export type LabelPosition = MarkPointComponentOption['label']['position']; -export type ReferencePointStoreState = { +export type ReferencePointConfig = { data?: any; x?: number | string; y?: number | string; label?: string; symbol?: Symbol; - color?: Color; - labelColor?: Color; - symbolColor?: Color; + color?: ReferenceColor; + labelColor?: ReferenceColor; + symbolColor?: ReferenceColor; symbolSize?: number; symbolOpacity?: number; symbolBorderWidth?: number; @@ -41,11 +32,8 @@ export type ReferencePointStoreState = { align?: 'left' | 'center' | 'right'; bold?: boolean; italic?: boolean; - error?: string; }; -export type ReferencePointStore = Writable; - -export type ReferencePointChartData = MarkPointComponentOption['data'][number] & { - evidenceSeriesType: 'reference_point'; +export type ReferencePointStoreValue = { + error?: string; }; diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/colors.js b/packages/ui/core-components/src/lib/unsorted/viz/references/colors.js deleted file mode 100644 index 3857dad0f6..0000000000 --- a/packages/ui/core-components/src/lib/unsorted/viz/references/colors.js +++ /dev/null @@ -1,34 +0,0 @@ -// @ts-check - -import { uiColours } from '@evidence-dev/component-utilities/colours'; - -/** @satisfies {Record} */ -export const colorList = { - red: { symbolColor: '#b04646', labelColor: '#b04646' }, - green: { symbolColor: uiColours.green700, labelColor: uiColours.green700 }, - yellow: { symbolColor: uiColours.yellow600, labelColor: uiColours.yellow700 }, - grey: { symbolColor: uiColours.grey500, labelColor: uiColours.grey600 }, - blue: { symbolColor: uiColours.blue500, labelColor: uiColours.blue500 } -}; - -// Hack to prevent typescript from reducing this type to just `string` -// See: https://stackoverflow.com/a/61048124 -/** @typedef {keyof typeof colorList | (string & {})} Color */ - -/** - * @param {{ color?: string; labelColor?: string; symbolColor?: string }} colors - * @returns {{labelColor?: string, symbolColor?: string}} - */ -export const getLineAndSymbolColors = (colors) => { - let labelColor = colors.labelColor ?? colors.color; - if (labelColor && labelColor in colorList) { - labelColor = colorList[/** @type {keyof typeof colorList} */ (labelColor)].labelColor; - } - - let symbolColor = colors.symbolColor ?? colors.color; - if (symbolColor && symbolColor in colorList) { - symbolColor = colorList[/** @type {keyof typeof colorList} */ (symbolColor)].symbolColor; - } - - return { labelColor, symbolColor }; -}; diff --git a/packages/ui/core-components/src/lib/unsorted/viz/references/types.js b/packages/ui/core-components/src/lib/unsorted/viz/references/types.js new file mode 100644 index 0000000000..94cae9c2f9 --- /dev/null +++ b/packages/ui/core-components/src/lib/unsorted/viz/references/types.js @@ -0,0 +1,15 @@ +export const PRESET_COLORS = /** @type {const} */ (['red', 'green', 'yellow', 'grey', 'blue']); + +/** @typedef {typeof PRESET_COLORS[number]} PresetColor */ + +/** + * @param {unknown} s + * @returns {s is PresetColor} + */ +export const isPresetColor = (s) => PRESET_COLORS.includes(s); + +// Hack to prevent typescript from reducing this type to just `string` +// See: https://stackoverflow.com/a/61048124 +/** @typedef {PresetColor | (string & {})} ReferenceColor */ + +/** @typedef {'circle' | 'rect' | 'roundRect' | 'triangle' | 'diamond' | 'pin' | 'arrow' | 'none' | `path://${string}`} Symbol */ diff --git a/packages/ui/core-components/src/lib/utils.js b/packages/ui/core-components/src/lib/utils.js index 61b5a056ec..ee7c7a6f4c 100644 --- a/packages/ui/core-components/src/lib/utils.js +++ b/packages/ui/core-components/src/lib/utils.js @@ -65,7 +65,19 @@ export const flyAndScale = (node, params = { y: -8, x: 0, start: 0.95, duration: * @returns {number | undefined} */ export const toNumber = (value) => { - if (value === null) return undefined; if (typeof value === 'undefined') return undefined; return Number(value); }; + +/** + * @param {unknown} value + * @returns {boolean | undefined} + */ +export const toBoolean = (value) => { + if (typeof value === 'undefined') return undefined; + if (typeof value === 'string') { + if (value.toLowerCase() === 'true') return true; + if (value.toLowerCase() === 'false') return false; + } + return Boolean(value); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4b773ba2a..fdd1cc551f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5708,6 +5708,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/cache@2.12.0(@parcel/core@2.12.0): @@ -5721,6 +5722,8 @@ packages: '@parcel/logger': 2.12.0 '@parcel/utils': 2.12.0 lmdb: 2.8.5 + transitivePeerDependencies: + - '@swc/helpers' dev: true /@parcel/codeframe@2.12.0: @@ -5737,6 +5740,7 @@ packages: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/config-default@2.12.0(@parcel/core@2.12.0)(postcss@8.4.39)(typescript@5.4.2): @@ -5881,6 +5885,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/node-resolver-core@3.3.0(@parcel/core@2.12.0): @@ -5911,6 +5916,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/optimizer-htmlnano@2.12.0(@parcel/core@2.12.0)(postcss@8.4.39)(typescript@5.4.2): @@ -5924,6 +5930,7 @@ packages: svgo: 2.8.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' - cssnano - postcss - purgecss @@ -5946,6 +5953,8 @@ packages: '@parcel/rust': 2.12.0 '@parcel/utils': 2.12.0 '@parcel/workers': 2.12.0(@parcel/core@2.12.0) + transitivePeerDependencies: + - '@swc/helpers' dev: true /@parcel/optimizer-svgo@2.12.0(@parcel/core@2.12.0): @@ -5958,6 +5967,7 @@ packages: svgo: 2.8.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/optimizer-swc@2.12.0(@parcel/core@2.12.0): @@ -6007,6 +6017,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/packager-html@2.12.0(@parcel/core@2.12.0): @@ -6020,6 +6031,7 @@ packages: posthtml: 0.16.6 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/packager-js@2.12.0(@parcel/core@2.12.0): @@ -6036,6 +6048,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/packager-raw@2.12.0(@parcel/core@2.12.0): @@ -6045,6 +6058,7 @@ packages: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/packager-svg@2.12.0(@parcel/core@2.12.0): @@ -6057,6 +6071,7 @@ packages: posthtml: 0.16.6 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/packager-ts@2.12.0(@parcel/core@2.12.0): @@ -6075,6 +6090,7 @@ packages: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/plugin@2.12.0(@parcel/core@2.12.0): @@ -6116,6 +6132,7 @@ packages: '@parcel/utils': 2.12.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/reporter-tracer@2.12.0(@parcel/core@2.12.0): @@ -6138,6 +6155,7 @@ packages: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/runtime-browser-hmr@2.12.0(@parcel/core@2.12.0): @@ -6148,6 +6166,7 @@ packages: '@parcel/utils': 2.12.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/runtime-js@2.12.0(@parcel/core@2.12.0): @@ -6160,6 +6179,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/runtime-react-refresh@2.12.0(@parcel/core@2.12.0): @@ -6172,6 +6192,7 @@ packages: react-refresh: 0.9.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/runtime-service-worker@2.12.0(@parcel/core@2.12.0): @@ -6183,6 +6204,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/rust@2.12.0: @@ -6211,6 +6233,7 @@ packages: semver: 7.6.2 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-css@2.12.0(@parcel/core@2.12.0): @@ -6226,6 +6249,7 @@ packages: nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-html@2.12.0(@parcel/core@2.12.0): @@ -6243,6 +6267,7 @@ packages: srcset: 4.0.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-image@2.12.0(@parcel/core@2.12.0): @@ -6256,6 +6281,8 @@ packages: '@parcel/utils': 2.12.0 '@parcel/workers': 2.12.0(@parcel/core@2.12.0) nullthrows: 1.1.1 + transitivePeerDependencies: + - '@swc/helpers' dev: true /@parcel/transformer-inline-string@2.12.0(@parcel/core@2.12.0): @@ -6295,6 +6322,7 @@ packages: json5: 2.2.3 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-postcss@2.12.0(@parcel/core@2.12.0): @@ -6311,6 +6339,7 @@ packages: semver: 7.6.2 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-posthtml@2.12.0(@parcel/core@2.12.0): @@ -6326,6 +6355,7 @@ packages: semver: 7.6.2 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-raw@2.12.0(@parcel/core@2.12.0): @@ -6335,6 +6365,7 @@ packages: '@parcel/plugin': 2.12.0(@parcel/core@2.12.0) transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-react-refresh-wrap@2.12.0(@parcel/core@2.12.0): @@ -6346,6 +6377,7 @@ packages: react-refresh: 0.9.0 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-svg@2.12.0(@parcel/core@2.12.0): @@ -6362,6 +6394,7 @@ packages: semver: 7.6.2 transitivePeerDependencies: - '@parcel/core' + - '@swc/helpers' dev: true /@parcel/transformer-typescript-types@2.12.0(@parcel/core@2.12.0)(typescript@5.4.2): diff --git a/sites/docs/pages/components/annotations.md b/sites/docs/pages/components/annotations.md index 36be6c1181..e7cb01c99e 100644 --- a/sites/docs/pages/components/annotations.md +++ b/sites/docs/pages/components/annotations.md @@ -384,45 +384,125 @@ If you provide `[x, y]` and `[x2, y2]`, coordinates must fall within the chart's ### Styling - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + # Reference Area @@ -637,45 +717,117 @@ A reference area can be produced by defining values inline or by supplying a dat ### Styling - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + # Reference Point