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