diff --git a/.changeset/empty-drinks-push.md b/.changeset/empty-drinks-push.md new file mode 100644 index 000000000..dc24f1388 --- /dev/null +++ b/.changeset/empty-drinks-push.md @@ -0,0 +1,12 @@ +--- +"@latitude-data/svelte": minor +"@latitude-data/client": minor +"@latitude-data/server": minor +--- + +- Add sort prop to charts +- Add title and description to charts +- Fix truncated label in charts +- Animate chart data changes +- Improve chart error display (will be changed in a future PR) +- Display generic error on charts when a query fails in production diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..79bdb1b97 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.7.0 diff --git a/apps/server/package.json b/apps/server/package.json index 19470c822..33f0b195a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -11,7 +11,7 @@ "preview": "vite preview", "prettier": "prettier --write src/**/*.ts src/**/*.svelte", "test": "svelte-kit sync && vitest --run", - "test:watch": "vitest", + "test:watch": "vitest --watch", "lint": "eslint .", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --output human-verbose", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", diff --git a/apps/server/src/routes/api/queries/[...query]/+server.ts b/apps/server/src/routes/api/queries/[...query]/+server.ts index 39bc1dca2..7c4ebb837 100644 --- a/apps/server/src/routes/api/queries/[...query]/+server.ts +++ b/apps/server/src/routes/api/queries/[...query]/+server.ts @@ -22,7 +22,9 @@ export async function GET({ status: 200, }) } catch (e) { - return handleError(e as Error) + const isPro = import.meta.env.PROD + const clientError = isPro ? new Error('There was an error in this query') : e as Error + return handleError(clientError) } } diff --git a/apps/server/src/routes/api/queries/[...query]/server.test.ts b/apps/server/src/routes/api/queries/[...query]/server.test.ts index 91d7396f3..1354ad7c7 100644 --- a/apps/server/src/routes/api/queries/[...query]/server.test.ts +++ b/apps/server/src/routes/api/queries/[...query]/server.test.ts @@ -133,4 +133,18 @@ describe('GET endpoint', () => { expect(response.status).toBe(404) expect(await response.text()).toBe('Query file not found') }) + + it('return generic error when is production', async () => { + import.meta.env.PROD = true + mockRunQuery.mockRejectedValue(new Error('Query execution failed')) + + const response = await GET({ + params: { query: 'testQuery' }, + url: new URL('http://localhost'), + }) + + expect(response.status).toBe(500) + expect(await response.text()).toBe('There was an error in this query') + }) + }) diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index 54bb3e589..c08607202 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -1,5 +1,6 @@ import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' + // eslint-disable-next-line // @ts-ignore import autoImport from '@latitude-data/sveltekit-autoimport' diff --git a/docs/snippets/charts/common.mdx b/docs/snippets/charts/common.mdx new file mode 100644 index 000000000..62f9b308c --- /dev/null +++ b/docs/snippets/charts/common.mdx @@ -0,0 +1,208 @@ +## Properties +To configure and customize the chart, you can add properties, some of which are required and some of which are optional. + + + The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it + + + + Specify a column name to return from your query. This will be the data in the horizontal (x) axis. + + [See the section Axis X and Y](#axis-x-and-y-options) to learn more about the options. + + + + Specify the array of series you want to visualize. Each serie has the column name that your query and other properties. + + [See the section Axis X and Y](#axis-x-and-y-options) to learn more about the options. + + + + The title of your chart. + + + + You can add a brief description of your chart + + + + Specify the order of the data returned by your query. It can be a string with the column name or an object with the column name and the order. + + [See the section order](#order-options) to learn more about the order options + + + + It allows you to customize how the information is displayed in the given axis. It contains several options. + + [See the section xAxisFormat](#axis-x-and-y-options) to learn more about the options. + + + + It allows you to customize how the information is displayed in the given axis. It contains several options. + + [See the section yAxisFormat](#axis-x-and-y-options) to learn more about the options. + + + + Specifies whether or not to show an animation when the page loads. + + + + A custom name to display as the title of your (x) axis. + + + + A custom name to display as the title of your (y) axis. + + + + Swap the axis orientation. Visually swap the (x) axis with the (y) axis. + + + + Contains some options to customize your chart. + + [See the Config section](#config) to learn more about the options. + + +### Axis X and Y Options + +Both `x` and `y` can contain multiple data series, so instead of specifying just a column name, you need to create a list of series using the following properties: + + + Specify a column name returned by your query. + + + + + Specifies the group within an axis. Imagine 3 series that belong to the left side of the Y-axis, 2 of them need one specific format but the rest need another. axisIndex helps to define different formats for series on the same side of the axis. + + The options for this property are numbers: `0`, `1`, `2`... + + How this link to axisFormat is by the order of the object in the arra of axisFormat. `1` will be the second object in the list, `0` is the first object in the list... + + +Example of syntax for multiple series: + +``` + y={[ + {name: 'column_name'}, + {name: 'column_name', axisIndex: '0'}, + {name: 'column_name', axisIndex: '1'} + ]} +``` + +#### Pivoted table +If you have a pivoted table where you have multiple columns with names like sum_users, sum_amount, sum_orders, (...) and you need to add all the columns without typing all the names you can use: + +- `prefix_` + `*` +- `*` + `suffix` + +Example to add all columns starting wiht `sum_`: +``` + y={[ + {name: 'sum_*'}, + ]} +``` + +### Order Options +The order property can be a string with the column name or an object with the column name and the order. + + + The column name to order the data. + + + + The column name to order the data. Options are: `asc` and `desc` + + + + When "numeric" is compared with "non-numeric-string", or either of them is compared with other types of value, they are not comparable. So we call the latter one as "incomparable" and treat it as "min value" or "max value" according to the prop `incomparable`: 'min' | 'max'. This feature usually helps to decide whether to put the empty values (like `null`, `undefined`, `NaN`, `''`, `'-'`) or other illegal values to the head or tail. + + + + If intending to sort time values (JS Date instance or time string like `2012-03-12 11:13:54`), parser: `time` should be specified. + If intending to sort values with unit suffix (like `'33%'`, `'16px'`), need to use parser: `number`. + + +Example of syntax for multiple column order: + +``` + order={[ + {name: 'column_a', order: 'asc'}, + {name: 'column_b', order: 'desc', incomparable: 'min', parser: 'time' }, + ]} +``` + +### Axis Format + +Example of different axis format. The order of the array will determine the axisIndex where `0` = the first format in the list. + +``` + yAxisFormat={[ + {type: 'value', stack: 'true'}, + {type: 'category', stack: 'false'}, + {type: 'value', stack: 'false;} + ]} +``` + + The format of the data displayed in the axis. The options are: + - `category` + - `value` + - `time` + - `log` + + + + Display the data series of the given axis on top of each other, visualizing cumulative totals or comparisons over time. + + + + Display the axis info (labels, numbers...) in the chart or not. + + + + Allow to display the axis values with a tilt to avoid overlapping between them. + + + + The axis title to display. + + + + Whether or not to show the title of the axis. + + + + Specifies whether or not to show the value indicator with a split line. + + + + Specifies the side of the axis + - `start`: in axis (x) means left and for (y) axis means top. + - `end`: in axis (x) means right and for (y) axis means bottom. + + +### Config +Allows you to add more configuration and customize your chart. + + + Show a dot or not at the intersection of the values. + + + + Display a zoom control for the displayed data. + + + + Display the values directly on the chart. + + + + Display the color legend for each series displayed. + + + + Add a visual pattern to each series to help colorblind people distinguish them. + + diff --git a/docs/visualizations/others/funnel-chart.mdx b/docs/visualizations/others/funnel-chart.mdx index 3528e93d9..d028b5ea1 100644 --- a/docs/visualizations/others/funnel-chart.mdx +++ b/docs/visualizations/others/funnel-chart.mdx @@ -9,12 +9,11 @@ The Funnel chart visualizes sequential stages in a process, showing decreasing q ## Preparation The query `.sql` file that the Funnel chart accepts must contain only 2 columns, one for the labels and another one for the values. -| labels | values | -| ----------- | -------- | -| Pending | 245 | -| In Progress | 120 | -| Closed | 32 | - +| values | labels | +| -------- | ----------- | +| 245 | Pending | +| 120 | In Progress | +| 32 | Closed | ## Syntax You can use `` to add it to your view. Within the tag you must add the properties to configure the chart. @@ -22,11 +21,9 @@ You can use `` to add it to your view. Within the tag you mus ``` js ``` @@ -37,13 +34,13 @@ To configure and customize the chart, you can add properties, some of which are The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it - - Contains some options to customize your chart. - - [See the Config section](#config) to learn more about the options. + + The order in which the data will be displayed. It can be `ascending` or `descending`. Default `descending`. -### Config + + The orientation of the chart. It can be `vertical` or `horizontal`. Default `vertical`. + Whether the color of each layer is a gradient or whether each layer is a different color. diff --git a/docs/visualizations/two-axes-charts/area-chart.mdx b/docs/visualizations/two-axes-charts/area-chart.mdx index 5a7d66c18..e00cde30e 100644 --- a/docs/visualizations/two-axes-charts/area-chart.mdx +++ b/docs/visualizations/two-axes-charts/area-chart.mdx @@ -2,6 +2,7 @@ title: 'Area Chart' description: 'How to create and configure the Area chart' --- +import CommonChartsDocs from '/snippets/charts/common.mdx' ## Introduction An Area Chart visualizes quantitative data over time through filled areas below the line, highlighting volume and trends. It's useful for comparing multiple data sets and understanding the relative significance of each. @@ -12,180 +13,20 @@ You can use `` to add it in your view. Within the tag you must ``` js ``` -## Properties -To configure and customize the chart, you can add properties, some of which are required and some of which are optional. - - - The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it - - - - Specify a column name to return from your query. This will be the data in the horizontal (x) axis. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - Specify the array of series you want to visualize. Each serie has the column name that your query and other properties. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section xAxisFormat](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section yAxisFormat](#x-and-y-options) to learn more about the options. - - - - Specifies whether or not to show an animation when the page loads. - - - - A custom name to display as the title of your (x) axis. - - - - A custom name to display as the title of your (y) axis. - - - - Swap the axis orientation. Visually swap the (x) axis with the (y) axis. - - - - Contains some options to customize your chart. - - [See the Config section](#config) to learn more about the options. - - -### Axis X and Y Options - -Both `x` and `y` can contain multiple data series, so instead of specifying just a column name, you need to create a list of series using the following properties: - - - Specify a column name returned by your query. - - - - - Specifies the group within an axis. Imagine 3 series that belong to the left side of the Y-axis, 2 of them need one specific format but the rest need another. axisIndex helps to define different formats for series on the same side of the axis. - - The options for this property are numbers: `0`, `1`, `2`... - - How this link to axisFormat is by the order of the object in the arra of axisFormat. `1` will be the second object in the list, `0` is the first object in the list... - - -Example of syntax for multiple series: - -``` - y={[ - {name: 'column_name'}, - {name: 'column_name', axisIndex: '0'}, - {name: 'column_name', axisIndex: '1'} - ]} -``` - -#### Pivoted table -If you have a pivoted table where you have multiple columns with names like sum_users, sum_amount, sum_orders, (...) and you need to add all the columns without typing all the names you can use: - -- `prefix_` + `*` -- `*` + `suffix` - -Example to add all columns starting wiht `sum_`: -``` - y={[ - {name: 'sum_*'}, - ]} -``` -### Axis Format - -Example of different axis format. The order of the array will determine the axisIndex where `0` = the first format in the list. - -``` - yAxisFormat={[ - {type: 'value', stack: 'true'}, - {type: 'category', stack: 'false'}, - {type: 'value', stack: 'false;} - ]} -``` - - The format of the data displayed in the axis. The options are: - - `category` - - `value` - - `time` - - `log` - - - - Display the data series of the given axis on top of each other, visualizing cumulative totals or comparisons over time. - - - - Display the axis info (labels, numbers...) in the chart or not. - - - - Allow to display the axis values with a tilt to avoid overlapping between them. - - - - The axis title to display. - - - - Whether or not to show the title of the axis. - - - - Specifies whether or not to show the value indicator with a split line. - - - - Specifies the side of the axis - - `start`: in axis (x) means left and for (y) axis means top. - - `end`: in axis (x) means right and for (y) axis means bottom. - - -### Config -Allows you to add more configuration and customize your chart. - - - Show a dot or not at the intersection of the values. - - - - Display a zoom control for the displayed data. - - - - Display the values directly on the chart. - - - - Display the color legend for each series displayed. - - - - Add a visual pattern to each series to help colorblind people distinguish them. - - + diff --git a/docs/visualizations/two-axes-charts/bar-chart.mdx b/docs/visualizations/two-axes-charts/bar-chart.mdx index 00bb84195..c9b232541 100644 --- a/docs/visualizations/two-axes-charts/bar-chart.mdx +++ b/docs/visualizations/two-axes-charts/bar-chart.mdx @@ -2,6 +2,7 @@ title: 'Bar Chart' description: 'How to create and configure the Bar chart' --- +import CommonChartsDocs from '/snippets/charts/common.mdx' ## Introduction The Bar chart represents quantitative data with vertical or horizontal bars, showing comparisons across categories or time. It highlights differences in values, making it ideal for comparing multiple datasets and identifying trends or outliers. @@ -12,172 +13,17 @@ You can use `` to add it in your view. Within the tag you must a ``` js ``` -## Properties -To configure and customize the chart, you can add properties, some of which are required and some of which are optional. - - - The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it - - - - Specify a column name to return from your query. This will be the data in the horizontal (x) axis. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - Specify the array of series you want to visualize. Each serie has the column name that your query and other properties. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section xAxisFormat](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section yAxisFormat](#x-and-y-options) to learn more about the options. - - - - Specifies whether or not to show an animation when the page loads. - - - - A custom name to display as the title of your (x) axis. - - - - A custom name to display as the title of your (y) axis. - - - - Swap the axis orientation. Visually swap the (x) axis with the (y) axis. - - - - Contains some options to customize your chart. - - [See the Config section](#config) to learn more about the options. - - -### Axis X and Y Options - -Both `x` and `y` can contain multiple data series, so instead of specifying just a column name, you need to create a list of series using the following properties: - - - Specify a column name returned by your query. - - - - Specifies the group within an axis. Imagine 3 series that belong to the left side of the Y-axis, 2 of them need one specific format but the rest need another. axisIndex helps to define different formats for series on the same side of the axis. - - The options for this property are numbers: `0`, `1`, `2`... - - How this link to axisFormat is by the order of the object in the arra of axisFormat. `1` will be the second object in the list, `0` is the first object in the list... - - -Example of syntax for multiple series: - -``` - y={[ - {name: 'column_name'}, - {name: 'column_name' axisIndex: '0'}, - {name: 'column_name' axisIndex: '1'} - ]} -``` - -#### Pivoted table -If you have a pivoted table where you have multiple columns with names like sum_users, sum_amount, sum_orders, (...) and you need to add all the columns without typing all the names you can use: - -- `prefix_` + `*` -- `*` + `suffix` - -Example to add all columns starting wiht `sum_`: -``` - y={[ - {name: 'sum_*'}, - ]} -``` -### Axis Format - -Example of different axis format. The order of the array will determine the axisIndex where `0` = the first format in the list. - -``` - yAxisFormat={[ - {type: 'value', stack: 'true'}, - {type: 'category', stack: 'false'}, - {type: 'value', stack: 'false;} - ]} -``` - - The format of the data displayed in the axis. The options are: - - `category` - - `value` - - `time` - - `log` - - - - Display the data series of the given axis on top of each other, visualizing cumulative totals or comparisons over time. - - - - Display the axis info (labels, numbers...) in the chart or not. - - - - Allow to display the axis values with a tilt to avoid overlapping between them. - - - - The axis title to display. - - - - Whether or not to show the title of the axis. - - - - Specifies whether or not to show the value indicator with a split line. - - - - Specifies the side of the axis - - `start`: in axis (x) means left and for (y) axis means top. - - `end`: in axis (x) means right and for (y) axis means bottom. - - -### Config -Allows you to add more configuration and customize your chart. - - - Display a zoom control for the displayed data. - - - - Display the values directly on the chart. - - - - Display the color legend for each series displayed. - - - - Add a visual pattern to each series to help colorblind people distinguish them. - - + diff --git a/docs/visualizations/two-axes-charts/line-chart.mdx b/docs/visualizations/two-axes-charts/line-chart.mdx index 07757bccb..98eb64c60 100644 --- a/docs/visualizations/two-axes-charts/line-chart.mdx +++ b/docs/visualizations/two-axes-charts/line-chart.mdx @@ -2,6 +2,7 @@ title: 'Line Chart' description: 'How to create and configure the Line chart' --- +import CommonChartsDocs from '/snippets/charts/common.mdx' ## Introduction The Line chart plots data points connected by straight lines to visualize trends over time or categories. It's helpful for showing changes and patterns, making it ideal for comparing different data sets and forecasting future values. @@ -12,8 +13,11 @@ You can use `` to add it in your view. Within the tag you must ``` js ` to add it in your view. Within the tag you must }} /> ``` - -## Properties -To configure and customize the chart, you can add properties, some of which are required and some of which are optional. - - - The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it - - - - Specify a column name to return from your query. This will be the data in the horizontal (x) axis. - - [See the section Axis X and Y](#axis-x-and-y-options) to learn more about the options. - - - - Specify the array of series you want to visualize. Each serie has the column name that your query and other properties. - - [See the section Axis X and Y](#axis-x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section xAxisFormat](#axis-x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section yAxisFormat](#axis-x-and-y-options) to learn more about the options. - - - - Specifies whether or not to show an animation when the page loads. - - - - A custom name to display as the title of your (x) axis. - - - - A custom name to display as the title of your (y) axis. - - - - Swap the axis orientation. Visually swap the (x) axis with the (y) axis. - - - - Contains some options to customize your chart. - - [See the Config section](#config) to learn more about the options. - - -### Axis X and Y Options - -Both `x` and `y` can contain multiple data series, so instead of specifying just a column name, you need to create a list of series using the following properties: - - - Specify a column name returned by your query. - - - - Specifies the group within an axis. Imagine 3 series that belong to the left side of the Y-axis, 2 of them need one specific format but the rest need another. axisIndex helps to define different formats for series on the same side of the axis. - - The options for this property are numbers: `0`, `1`, `2`... - - How this link to axisFormat is by the order of the object in the arra of axisFormat. `1` will be the second object in the list, `0` is the first object in the list... - - -Example of syntax for multiple series: - -``` - y={[ - {name: 'column_name'}, - {name: 'column_name' axisIndex: '0'}, - {name: 'column_name' axisIndex: '1'} - ]} -``` - -#### Pivoted table -If you have a pivoted table where you have multiple columns with names like sum_users, sum_amount, sum_orders, (...) and you need to add all the columns without typing all the names you can use: - -- `prefix_` + `*` -- `*` + `suffix` - -Example to add all columns starting wiht `sum_`: -``` - y={[ - {name: 'sum_*'}, - ]} -``` -### Axis Format - -Example of different axis format. The order of the array will determine the axisIndex where `0` = the first format in the list. - -``` - yAxisFormat={[ - {type: 'value', stack: 'true'}, - {type: 'category', stack: 'false'}, - {type: 'value', stack: 'false;} - ]} -``` - - The format of the data displayed in the axis. The options are: - - `category` - - `value` - - `time` - - `log` - - - - Display the data series of the given axis on top of each other, visualizing cumulative totals or comparisons over time. - - - - Display the axis info (labels, numbers...) in the chart or not. - - - - Allow to display the axis values with a tilt to avoid overlapping between them. - - - - The axis title to display. - - - - Whether or not to show the title of the axis. - - - - Specifies whether or not to show the value indicator with a split line. - - - - Specifies the side of the axis - - `start`: in axis (x) means left and for (y) axis means top. - - `end`: in axis (x) means right and for (y) axis means bottom. - - -### Config -Allows you to add more configuration and customize your chart. - - - Show a dot or not at the intersection of the values. - - - - Display a zoom control for the displayed data. - - - - Display the values directly on the chart. - - - - Display the color legend for each series displayed. - - - - Add a visual pattern to each series to help colorblind people distinguish them. - - + diff --git a/docs/visualizations/two-axes-charts/mixed-types-chart.mdx b/docs/visualizations/two-axes-charts/mixed-types-chart.mdx index 276c864e2..8719d91dc 100644 --- a/docs/visualizations/two-axes-charts/mixed-types-chart.mdx +++ b/docs/visualizations/two-axes-charts/mixed-types-chart.mdx @@ -2,6 +2,7 @@ title: 'Mixed Types Chart' description: 'How to create and configure a Mixed Types Chart' --- +import CommonChartsDocs from '/snippets/charts/common.mdx' ## Introduction A mixed chart combines different chart types (for example, bars and lines) to present complementary data and give more context in a single visualization. @@ -12,187 +13,21 @@ You can use `` to add it in your view. Within the tag you must ``` js ``` -## Properties -To configure and customize the chart, you can add properties, some of which are required and some of which are optional. - - -The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it - - - - Specify a column name to return from your query. This will be the data in the horizontal (x) axis. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - Specify the array of series you want to visualize. Each serie has the column name that your query and other properties. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section xAxisFormat](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section yAxisFormat](#x-and-y-options) to learn more about the options. - - - - Specifies whether or not to show an animation when the page loads. - - - - A custom name to display as the title of your (x) axis. - - - - A custom name to display as the title of your (y) axis. - - - - Swap the axis orientation. Visually swap the (x) axis with the (y) axis. - - - - Contains some options to customize your chart. - - [See the Config section](#config) to learn more about the options. - - -### Axis X and Y Options - -Both `x` and `y` can contain multiple data series, so instead of specifying just a column name, you need to create a list of series using the following properties: - - - Specify a column name returned by your query. - - - - The chart type of the serie. The options are: - - `line` - - `bar` - - `area` - - - - Specifies the group within an axis. Imagine 3 series that belong to the left side of the Y-axis, 2 of them need one specific format but the rest need another. axisIndex helps to define different formats for series on the same side of the axis. - - The options for this property are numbers: `0`, `1`, `2`... - - -Example of syntax for multiple series: - -``` - y={[ - {name: 'column_name', chartType: 'bar'}, - {name: 'column_name', chartType: 'line', axisIndex: 'start'}, - {name: 'column_name', chartType: 'area', axisIndex: 'end'} - ]} -``` - -#### Pivoted table -If you have a pivoted table where you have multiple columns with names like sum_users, sum_amount, sum_orders, (...) and you need to add all the columns without typing all the names you can use: - -- `prefix_` + `*` -- `*` + `suffix` - -Example to add all columns starting wiht `sum_`: -``` - y={[ - {name: 'sum_*', chartType: 'bar'}, - ]} -``` -### Axis Format - -Example of different axis format. The order of the array will determine the axisIndex where `0` = the first format in the list. - -``` - yAxisFormat={[ - {type: 'value', stack: 'true'}, - {type: 'category', stack: 'false'}, - {type: 'value', stack: 'false;} - ]} -``` - - - The format of the data displayed in the axis. The options are: - - `category` - - `value` - - `time` - - `log` - - - - Display the data series of the given axis on top of each other, visualizing cumulative totals or comparisons over time. - - - - Display the axis info (labels, numbers...) in the chart or not. - - - - Allow to display the axis values with a tilt to avoid overlapping between them. - - - - The axis title to display. - - - - Whether or not to show the title of the axis. - - - - Specifies whether or not to show the value indicator with a split line. - - - - Specifies the side of the axis - - `start`: in axis (x) means left and for (y) axis means top. - - `end`: in axis (x) means right and for (y) axis means bottom. - - -### Config -Allows you to add more configuration and customize your chart. - - - Show a dot or not at the intersection of the values. - - - - Display a zoom control for the displayed data. - - - - Display the values directly on the chart. - - - - Display the color legend for each series displayed. - - - - Add a visual pattern to each series to help colorblind people distinguish them. - - + diff --git a/docs/visualizations/two-axes-charts/scatter.mdx b/docs/visualizations/two-axes-charts/scatter.mdx index 557a83026..cd7b40ff4 100644 --- a/docs/visualizations/two-axes-charts/scatter.mdx +++ b/docs/visualizations/two-axes-charts/scatter.mdx @@ -2,6 +2,7 @@ title: 'Scatter Chart' description: 'How to create and configure the Scatter chart' --- +import CommonChartsDocs from '/snippets/charts/common.mdx' ## Introduction The Scatter chart displays data points on a grid, representing variable values with dots. It's excellent for revealing correlations, distributions, and outliers across datasets, making it crucial for statistical analysis and trend identification. @@ -12,176 +13,23 @@ You can use `` to add it in your view. Within the tag you mu ``` js ``` +## Scatter properties -## Properties -To configure and customize the chart, you can add properties, some of which are required and some of which are optional. - - - The name of your `.sql` file from which your chart will get the data. It's not necessary to specify the full path, just the name, because we will search all folders until we find it - - - - Specify a column name to return from your query. This will be the data in the horizontal (x) axis. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - Specify the array of series you want to visualize. Each serie has the column name that your query and other properties. - - [See the section Axis X and Y](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section xAxisFormat](#x-and-y-options) to learn more about the options. - - - - It allows you to customize how the information is displayed in the given axis. It contains several options. - - [See the section yAxisFormat](#x-and-y-options) to learn more about the options. - - - - Specifies whether or not to show an animation when the page loads. - - - - A custom name to display as the title of your (x) axis. - - - - A custom name to display as the title of your (y) axis. + + The column to use for the size of the dots. - - Swap the axis orientation. Visually swap the (x) axis with the (y) axis. - - - - Contains some options to customize your chart. - - [See the Config section](#config) to learn more about the options. - - -### Axis X and Y Options - -Both `x` and `y` can contain multiple data series, so instead of specifying just a column name, you need to create a list of series using the following properties: - - - Specify a column name returned by your query. - - - - Specifies the group within an axis. Imagine 3 series that belong to the left side of the Y-axis, 2 of them need one specific format but the rest need another. axisIndex helps to define different formats for series on the same side of the axis. - - The options for this property are numbers: `0`, `1`, `2`... - - How this link to axisFormat is by the order of the object in the arra of axisFormat. `1` will be the second object in the list, `0` is the first object in the list... - - -Example of syntax for multiple series: - -``` - y={[ - {name: 'column_name'}, - {name: 'column_name' axisIndex: '0'}, - {name: 'column_name' axisIndex: '1'} - ]} -``` - -#### Pivoted table -If you have a pivoted table where you have multiple columns with names like sum_users, sum_amount, sum_orders, (...) and you need to add all the columns without typing all the names you can use: - -- `prefix_` + `*` -- `*` + `suffix` - -Example to add all columns starting wiht `sum_`: -``` - y={[ - {name: 'sum_*'}, - ]} -``` -### Axis Format - -Example of different axis format. The order of the array will determine the axisIndex where `0` = the first format in the list. - -``` - yAxisFormat={[ - {type: 'value', stack: 'true'}, - {type: 'category', stack: 'false'}, - {type: 'value', stack: 'false;} - ]} -``` - - The format of the data displayed in the axis. The options are: - - `category` - - `value` - - `time` - - `log` - - - - Display the data series of the given axis on top of each other, visualizing cumulative totals or comparisons over time. - - - - Display the axis info (labels, numbers...) in the chart or not. - - - - Allow to display the axis values with a tilt to avoid overlapping between them. - - - - The axis title to display. - - - - Whether or not to show the title of the axis. - - - - Specifies whether or not to show the value indicator with a split line. - - - - Specifies the side of the axis - - `start`: in axis (x) means left and for (y) axis means top. - - `end`: in axis (x) means right and for (y) axis means bottom. - - -### Config -Allows you to add more configuration and customize your chart. - - - Show a dot or not at the intersection of the values. - - - - Display a zoom control for the displayed data. - - - - Display the values directly on the chart. - - - - Display the color legend for each series displayed. - - - - Add a visual pattern to each series to help colorblind people distinguish them. - + diff --git a/packages/client/core/src/theme/assets/latitude.css b/packages/client/core/src/theme/assets/latitude.css index 6011ac2cf..7d7a6a1ca 100644 --- a/packages/client/core/src/theme/assets/latitude.css +++ b/packages/client/core/src/theme/assets/latitude.css @@ -3,6 +3,28 @@ @tailwind utilities; @layer base { + .custom-scrollbar { + /** Make scrollbar to not take space **/ + overflow: overlay !important; + } + .custom-scrollbar::-webkit-scrollbar { + @apply w-5 h-5; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + @apply bg-gray-400; + @apply rounded-full bg-clip-padding border-solid; + @apply border-transparent border-[6px]; + } + + .custom-scrollbar::-webkit-scrollbar-track { + background-color: transparent; + } + + .custom-scrollbar::-webkit-scrollbar-corner { + background-color: transparent; + } + * { @apply border-border; } diff --git a/packages/client/core/src/theme/ui/alert/index.ts b/packages/client/core/src/theme/ui/alert/index.ts index 7de61b5fc..582be4c41 100644 --- a/packages/client/core/src/theme/ui/alert/index.ts +++ b/packages/client/core/src/theme/ui/alert/index.ts @@ -3,6 +3,11 @@ import { cn } from '../../utils' export type Variant = VariantProps['variant'] export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +export type Props = { + variant: Variant + scrollable?: boolean + className?: string | null +} export const alertVariants = tv({ base: 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', @@ -21,14 +26,15 @@ export const alertVariants = tv({ function rootCssClass({ variant, className, -}: { - variant: Variant - className?: string | null -}) { + scrollable = false +}: Props) { return cn( alertVariants({ variant }), 'text-sm [&_p]:leading-relaxed', - className + className, + { + 'overflow-y-auto custom-scrollbar': scrollable, + } ) } diff --git a/packages/client/core/src/theme/ui/card/index.ts b/packages/client/core/src/theme/ui/card/index.ts index 2eca64f67..c16fe3cb1 100644 --- a/packages/client/core/src/theme/ui/card/index.ts +++ b/packages/client/core/src/theme/ui/card/index.ts @@ -1,13 +1,48 @@ +import { tv, type VariantProps } from 'tailwind-variants' import { cn } from '../../utils' +type CardType = VariantProps['type'] +const NORMAL_PADDING = 'p-6' + +const cardRootVariants = tv({ + base: 'rounded-xl bg-card text-card-foreground ', + variants: { + type: { + normal: 'border shadow', + invisible: '' + }, + } +}) + +const cardHeaderVariants = tv({ + base: 'flex flex-col space-y-1.5', + variants: { + type: { + normal: NORMAL_PADDING, + invisible: 'pb-6' + } + } +}) + +const cardContentVariants = tv({ + base: '', + variants: { + type: { + normal: 'p-6 pt-0', + invisible: '' + } + } +}) + type ClassProps = { className?: string | null | undefined } +export type CardProps = ClassProps & { type?: CardType } -export function rootCssClass({ className }: ClassProps) { - return cn('rounded-xl border bg-card text-card-foreground shadow', className) +export function rootCssClass({ type = 'normal', className }: CardProps) { + return cn(cardRootVariants({ type }), className) } -export function headerCssClass({ className }: ClassProps) { - return cn('flex flex-col space-y-1.5 p-6', className) +export function headerCssClass({ type = 'normal', className }: CardProps) { + return cn(cardHeaderVariants({ type }) , className) } export function titleCssClass({ className }: ClassProps) { @@ -18,8 +53,8 @@ export function descriptionCssClass({ className }: ClassProps) { return cn('text-sm text-muted-foreground', className) } -export function contentCssClass({ className }: ClassProps) { - return cn('p-6 pt-0', className) +export function contentCssClass({ type = 'normal', className }: CardProps) { + return cn(cardContentVariants({ type }), className) } export function footerCssClass({ className }: ClassProps) { diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/__mocks__/data.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/__mocks__/data.ts index b17ce543a..6483d910d 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/__mocks__/data.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/__mocks__/data.ts @@ -15,14 +15,26 @@ const RAW_DATASET = [ ['IN', '78', '21', '11'], ] const OUTPUT_DATASET = RAW_DATASET.map((row) => - row.map((cell) => (cell === null ? 'null' : cell)), + row.map((cell) => { + if (cell === null) return 0 + + const cellVal = Number(cell) + if (isNaN(cellVal)) return cell + + return cellVal + }), ) as DBSource export const FIELDS = RAW_DATASET[0] as string[] export const SOURCE = RAW_DATASET.slice(1) as DBSource export const DATASET = { fields: FIELDS, source: SOURCE } export const testData = { animation: true, - dataset: [{ sourceHeader: true, source: OUTPUT_DATASET }], + animationEasing: 'cubicInOut', + animationEasingUpdate: 'cubicInOut', + dataset: [{ + dimensions: FIELDS, + source: OUTPUT_DATASET.slice(1) + }], xAxis: [ { show: true, @@ -124,7 +136,8 @@ export const testData = { areaStyle: { opacity: 0 }, connectNulls: true, label: { position: 'top', show: false, formatter: '{@1}' }, - encode: { x: 0, y: 1 }, + datasetIndex: 0, + encode: { x: 'country', y: 'movies_1' }, }, ], dataZoom: [], @@ -140,6 +153,6 @@ export const testData = { trigger: 'axis', axisPointer: { type: 'cross' }, }, - legend: { type: 'scroll', show: false, align: 'left', left: 40 }, - grid: { containLabel: true, left: 40, right: 8, top: 8, bottom: 32 }, + legend: { type: 'scroll', show: false, align: 'left', left: 48 }, + grid: { containLabel: true, left: 48, right: 32, top: 48, bottom: 32 }, } diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/getColumns.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/getColumns.ts index d8ff2633d..8dbc39cb7 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/getColumns.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/getColumns.ts @@ -17,9 +17,10 @@ function buildColumnString(column: string, chartType: CartesianChartType) { } } -function buildColumnObject(column: Column) { +function buildColumnObject(column: Column, chartType: CartesianChartType) { return { ...column, + chartType: column?.chartType ?? chartType, displayName: column?.displayName ?? column.name, axisIndex: column?.axisIndex ?? 0, } @@ -61,10 +62,10 @@ function completeColumn({ if (typeof column === 'string') { return completeStringColumn({ column, chartType, fields }) } - if (!isWildcard(column.name)) return buildColumnObject(column) + if (!isWildcard(column.name)) return buildColumnObject(column, chartType) return filterByWildcard(fields, column.name).map((name) => { - return buildColumnObject({ ...column, name }) + return buildColumnObject({ ...column, name }, chartType) }) } diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/index.test.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/index.test.ts index a725900d8..800ebab8a 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/index.test.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/index.test.ts @@ -55,7 +55,7 @@ describe('generateConfig', () => { ...testData.series[0], name: 'shows', xAxisIndex: 0, - encode: { x: 0, y: 2 }, + encode: { x: 'country', y: 'shows' }, label: { ...testData.series[0]?.label, formatter: '{@2}', @@ -96,7 +96,7 @@ describe('generateConfig', () => { { ...testData.series[0], xAxisIndex: 0, - encode: { x: 0, y: 3 }, + encode: { x: 'country', y: 'movies_2' }, label: { ...testData.series[0]?.label, formatter: '{@3}', @@ -107,7 +107,7 @@ describe('generateConfig', () => { { ...testData.series[0], name: 'shows', - encode: { x: 0, y: 2 }, + encode: { x: 'country', y: 'shows' }, label: { ...testData.series[0]?.label, formatter: '{@2}', @@ -153,9 +153,10 @@ describe('generateConfig', () => { it('only line charts', () => { expect( generateConfig({ + chartType: 'line', dataset: DATASET, x: 'country', - y: { name: 'movies_1', chartType: 'line' }, + y: { name: 'movies_1' }, }), ).toEqual({ ...testData, @@ -221,7 +222,7 @@ describe('generateConfig', () => { }, { ...testData.series[0], - encode: { x: 0, y: 2 }, + encode: { x: 'country', y: 'shows' }, label: { ...testData.series[0]?.label, formatter: '{@2}', @@ -252,54 +253,53 @@ describe('generateConfig', () => { ...testData, dataset: [ { - sourceHeader: true, + dimensions: ['country', 'movies_1', 'shows', 'movies_2'], source: [ - ['country', 'movies_1', 'shows', 'movies_2'], - ['CN', '0', '0.970873786407767', '0.02912621359223301'], - ['TW', '0', '0.970873786407767', '0.02912621359223301'], + ['CN', 0, 0.970873786407767, 0.02912621359223301], + ['TW', 0, 0.970873786407767, 0.02912621359223301], [ 'KR', - '0.019417475728155338', - '0.9514563106796117', - '0.02912621359223301', + 0.019417475728155338, + 0.9514563106796117, + 0.02912621359223301, ], [ 'JP', - '0.09803921568627451', - '0.8725490196078431', - '0.029411764705882353', + 0.09803921568627451, + 0.8725490196078431, + 0.029411764705882353, ], [ 'GB', - '0.12745098039215685', - '0.8431372549019608', - '0.029411764705882353', + 0.12745098039215685, + 0.8431372549019608, + 0.029411764705882353, ], [ 'ES', - '0.1568627450980392', - '0.8137254901960784', - '0.029411764705882353', + 0.1568627450980392, + 0.8137254901960784, + 0.029411764705882353, ], [ 'US', - '0.20588235294117646', - '0.7647058823529411', - '0.029411764705882353', + 0.20588235294117646, + 0.7647058823529411, + 0.029411764705882353, ], [ 'CA', - '0.23148148148148148', - '0.6944444444444444', - '0.07407407407407407', + 0.23148148148148148, + 0.6944444444444444, + 0.07407407407407407, ], [ 'AU', - '0.2616822429906542', - '0.6635514018691588', - '0.07476635514018691', + 0.2616822429906542, + 0.6635514018691588, + 0.07476635514018691, ], - ['IN', '0.7090909090909091', '0.19090909090909092', '0.1'], + ['IN', 0.7090909090909091, 0.19090909090909092, 0.1], ], }, ], @@ -311,7 +311,7 @@ describe('generateConfig', () => { }, { ...testData.series[0], - encode: { x: 0, y: 2 }, + encode: { x: 'country', y: 'shows' }, label: { ...testData.series[0]?.label, formatter: '{@2}', @@ -344,7 +344,7 @@ describe('generateConfig', () => { { ...testData.series[0], yAxisIndex: 0, - encode: { x: 1, y: 0 }, + encode: { x: 'movies_1', y: 'country' }, }, ], xAxis: [ diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/index.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/index.ts index 2e99b97fc..909e0f5b9 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/index.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/index.ts @@ -15,7 +15,7 @@ import { xAxisFormat, } from './types' -import { getDataset } from '../common/getDataset' +import { Sort, getDataset } from '../common/getDataset' import setDataZoom from './setDatazoom' import setLegend from '../common/setLegend' import setGrid from './serieUtils/setGrid' @@ -23,6 +23,38 @@ import { transformXAxis, transformYAxis } from './transformAxis' import transformCartesiansSeries from './transformSeries' import { ColumnConfig, getColumns } from './getColumns' +export type AnimationEasing = 'linear' + | 'quadraticIn' + | 'quadraticOut' + | 'quadraticInOut' + | 'cubicIn' + | 'cubicOut' + | 'cubicInOut' + | 'quarticIn' + | 'quarticOut' + | 'quarticInOut' + | 'quinticIn' + | 'quinticOut' + | 'quinticInOut' + | 'sinusoidalIn' + | 'sinusoidalOut' + | 'sinusoidalInOut' + | 'exponentialIn' + | 'exponentialOut' + | 'exponentialInOut' + | 'circularIn' + | 'circularOut' + | 'circularInOut' + | 'elasticIn' + | 'elasticOut' + | 'elasticInOut' + | 'backIn' + | 'backOut' + | 'backInOut' + | 'bounceIn' + | 'bounceOut' + | 'bounceInOut' + function swapAxisFn({ swapAxis, xAxis, @@ -86,9 +118,12 @@ const Y_FORMAT_DEFAULT = { export type Props = { animation?: boolean + animationEasing?: AnimationEasing + animationEasingUpdate?: AnimationEasing dataset: Dataset x: ColumnConfig y: ColumnConfig + sort?: Sort swapAxis?: boolean xTitle?: string yTitle?: string @@ -107,6 +142,8 @@ type CartesianProps = Props & { export default function generateConfig({ chartType = 'bar', animation = true, + animationEasing = 'cubicInOut', + animationEasingUpdate = 'cubicInOut', dataset, swapAxis = false, x, @@ -115,6 +152,7 @@ export default function generateConfig({ yTitle, xFormat = X_FORMAT_DEFAULT, yFormat = Y_FORMAT_DEFAULT, + sort, hiddenSeries = [], config = CONIFG_DEFAULTS, }: CartesianProps): EChartsOption | null | undefined { @@ -125,16 +163,15 @@ export default function generateConfig({ const xAxisList = Array.isArray(xFormat) ? xFormat.map(completeXAxis) : [completeXAxis(xFormat)] - const usePercentage = yAxisList[0]?.stack === 'normalized' + const normalizeValues = yAxisList[0]?.stack === 'normalized' const fields = dataset.fields const xColumns = getColumns({ column: x, chartType, fields }) const yColumns = getColumns({ column: y, chartType, fields }) - const generatedDataset = getDataset({ + const { datasets, datasetIndex } = getDataset({ dataset, - column: yColumns[0]?.name ?? '', - axisType: yAxisList[0]?.type ?? AxisType.value, - usePercentage, + normalizeValues, + sort }) const dataZoom = setDataZoom({ swapAxis, showZoom }) const grid = setGrid({ @@ -144,7 +181,6 @@ export default function generateConfig({ }) const legend = setLegend({ show: showLegend, left: grid.left ?? 0 }) - const { series, axisMetadata } = transformCartesiansSeries({ config, swapAxis, @@ -153,6 +189,7 @@ export default function generateConfig({ yColumns, dataset, hiddenSeries, + datasetIndex }) const rawXAxis = transformXAxis({ @@ -182,7 +219,9 @@ export default function generateConfig({ return { animation, - dataset: [generatedDataset], + animationEasing, + animationEasingUpdate, + dataset: datasets, xAxis, yAxis, series, diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setGrid.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setGrid.ts index 0522dd504..fb739f096 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setGrid.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setGrid.ts @@ -12,9 +12,9 @@ const setGrid = ({ }): GridOption => { return { containLabel: true, - left: SPACES.s10, - right: showZoom && swapAxis ? SPACES.s16 : SPACES.s2, - top: showLegend ? SPACES.s14 : SPACES.s2, + left: SPACES.s12, + right: showZoom && swapAxis ? SPACES.s16 : SPACES.s8, + top: showLegend ? SPACES.s14 : SPACES.s12, bottom: showZoom && !swapAxis ? SPACES.s20 : SPACES.s8, } } diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setSerie.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setSerie.ts index dd637e898..f58dc79e8 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setSerie.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/serieUtils/setSerie.ts @@ -19,6 +19,7 @@ type Props = { axisIndex: number source: DBSource config: ConfigProps + datasetIndex: number } export default function setSerie({ @@ -31,9 +32,9 @@ export default function setSerie({ dimension, source, config, + datasetIndex }: Props) { const serieIndex = fields.findIndex((f) => f === serieColumn) - const dimensionIndex = fields.findIndex((f) => f === dimension.name) const isArea = chartType === 'area' let serie: EchartsCartesianSeriesOption = { name: serieDisplayName || serieColumn, @@ -41,8 +42,9 @@ export default function setSerie({ areaStyle: { opacity: isArea ? 0.4 : 0 }, [swapAxis ? 'yAxisIndex' : 'xAxisIndex']: axisIndex, encode: swapAxis - ? { y: dimensionIndex, x: serieIndex } - : { x: dimensionIndex, y: serieIndex }, + ? { y: dimension.name, x: serieColumn } + : { x: dimension.name, y: serieColumn }, + datasetIndex } serie = setLineChartStyle({ serie, serieIndex, config }) diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/transformSeries.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/transformSeries.ts index 2531a2fdd..0bdeabb97 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/transformSeries.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/transformSeries.ts @@ -39,6 +39,7 @@ export default function transformCartesiansSeries({ dataset, hiddenSeries, config, + datasetIndex }: { xColumns: FullColumn[] yColumns: FullColumn[] @@ -47,6 +48,7 @@ export default function transformCartesiansSeries({ dataset: Dataset hiddenSeries: string[] config: ConfigProps + datasetIndex: number }) { const fields = dataset.fields const dimension = xColumns[0] @@ -85,6 +87,7 @@ export default function transformCartesiansSeries({ dimension, swapAxis, axisIndex, + datasetIndex, source, config, }) diff --git a/packages/client/core/src/theme/ui/chart/config/cartesian/types.ts b/packages/client/core/src/theme/ui/chart/config/cartesian/types.ts index c80d31b93..8c95f253a 100644 --- a/packages/client/core/src/theme/ui/chart/config/cartesian/types.ts +++ b/packages/client/core/src/theme/ui/chart/config/cartesian/types.ts @@ -16,11 +16,12 @@ export type ChartType = keyof typeof CHART_TYPES export type Column = { name: string - chartType: ChartType + chartType?: ChartType displayName?: string | null axisIndex?: number } export type FullColumn = Column & { + chartType: ChartType displayName: string | null axisIndex: number } diff --git a/packages/client/core/src/theme/ui/chart/config/common/getDataset.test.ts b/packages/client/core/src/theme/ui/chart/config/common/getDataset.test.ts new file mode 100644 index 000000000..30bfa0a38 --- /dev/null +++ b/packages/client/core/src/theme/ui/chart/config/common/getDataset.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from 'vitest' +import { getDataset } from './getDataset' + +describe('getDataset', () => { + it('should return DatasetOption with sourceHeader and stringified source', () => { + const mockDataset = { + fields: ['Country', 'Population', 'Area'], + source: [ + ['USA', '331', '9834'], + ['Canada', '38', '9984'] + ] + } + const result = getDataset({ dataset: mockDataset }) + expect(result).toEqual({ + datasetIndex: 0, + datasets: [{ + dimensions: ['Country', 'Population', 'Area'], + source: [ + ['USA', 331, 9834], + ['Canada', 38, 9984] + ] + }] + }) + }) + + it('should calculate percentages if usePercentage is true', () => { + const mockDataset = { + fields: ['Country', 'Population', 'Area'], + source: [ + ['USA', 331, 9834], + ['Canada', 38, 9984] + ] + } + const result = getDataset({ dataset: mockDataset, normalizeValues: true }) + const calculatePercentage = (value: number, total: number) => (value / total) + expect(result).toEqual({ + datasetIndex: 0, + datasets: [{ + dimensions: ['Country', 'Population', 'Area'], + source: [ + ['USA', calculatePercentage(331, 331 + 9834), calculatePercentage(9834, 331 + 9834)], + ['Canada', calculatePercentage(38, 38 + 9984), calculatePercentage(9984, 38 + 9984)] + ] + }] + }) + }) + + it('should sort dataset by column if axisType is time', () => { + const mockDataset = { + fields: ['Date', 'Value'], + source: [ + ['2021-01-02', 20], + ['2021-01-01', 10] + ] + } + const result = getDataset({ + dataset: mockDataset, + sort: { + column: 'Date', + order: 'asc', + parser: 'time' + } + }) + expect(result).toEqual({ + datasetIndex: 1, + datasets: [ + { + dimensions: ['Date', 'Value'], + source: [ + ['2021-01-02', 20], + ['2021-01-01', 10] + ] + }, + { + transform: { + type: 'sort', + config: [{ + dimension: 'Date', + order: 'asc', + incomparable: undefined, + parser: 'time' + }] + } + } + ] + }) + }) +}) + diff --git a/packages/client/core/src/theme/ui/chart/config/common/getDataset.ts b/packages/client/core/src/theme/ui/chart/config/common/getDataset.ts index cf84317a5..3677dfe1e 100644 --- a/packages/client/core/src/theme/ui/chart/config/common/getDataset.ts +++ b/packages/client/core/src/theme/ui/chart/config/common/getDataset.ts @@ -1,65 +1,92 @@ +import { isNaN } from 'lodash-es' import type { DatasetOption } from 'echarts/types/dist/shared' -import { DBSourceRow, Dataset } from '../../types' -import { AxisType } from '../types' +import { DBSource, DBSourceColumn, DBSourceRow, Dataset } from '../../types' -const STARTING_DATA_INDEX = 1 +function calculatePercentage(row: DBSourceRow) { + const total = row.reduce( + (sum, value) => { + const maybeNum = Number(value) + if (isNaN(maybeNum)) return sum -function calculateRowInPercentage(row: DBSourceRow) { - const rowData = row.slice(STARTING_DATA_INDEX) - const total = rowData.reduce( - (sum, value) => Number(sum) + Number(value), + return Number(sum) + maybeNum + }, 0, ) as number - if (total === 0) { - return [row[0]].concat(rowData.map(() => 0)) - } + if (total === 0) return row.map((val) => { + const maybeNum = Number(val) + if (isNaN(maybeNum)) return val + return 0 + }) + + return row.map((value) => { + const maybeNum = Number(value) + if (isNaN(maybeNum)) return value - return [row[0]].concat(rowData.map((value) => Number(value) / total)) + return maybeNum / total + }) } -type Value = string | number +type BaseSortItem = { + order: 'asc' | 'desc' + incomparable?: 'min' | 'max' + parser?: 'time' | 'number' | 'trim' +} +type TransformedSort = BaseSortItem & { dimension: string } +type SortItem = BaseSortItem & { column: string } + +export type Sort = string | SortItem | SortItem[] type Props = { dataset: Dataset - column?: string - axisType?: AxisType - usePercentage?: boolean + normalizeValues?: boolean + sort?: Sort +} + +function convertToNumberMaybe(item: DBSourceColumn) { + return isNaN(Number(item)) ? item : Number(item) +} + +function normalizeValues(source: DBSource, normalizeValues = false) { + if (!normalizeValues) return source.map((row) => row.map(convertToNumberMaybe)) + + return source.map(calculatePercentage) +} + +function completeSortItem(sort: string | SortItem): TransformedSort { + if (typeof sort === 'string') return { dimension: sort, order: 'asc'} + + return { + dimension: sort.column, + order: sort.order, + parser: sort.parser, + incomparable: sort.incomparable + } } -function calculateDatasetSource({ - dataset, - column, - axisType, - usePercentage, -}: Props) { - const forceSort = axisType === AxisType.time - const forceSortIndex = forceSort - ? dataset.fields.findIndex((field) => field === column) - : undefined - - // Sort manually by date if axis scale is Temporal - const sortedDataset = - forceSort && forceSortIndex !== undefined - ? dataset.source.sort((a: Value[], b: Value[]) => { - const aVal = a[forceSortIndex] - const bVal = b[forceSortIndex] - - if (typeof aVal !== 'string' || typeof bVal !== 'string') return 0 - - return new Date(aVal).getTime() - new Date(bVal).getTime() - }) - : dataset.source - - if (!usePercentage) return sortedDataset - - // Use relative percentage instead of absolute values if usePercentage is true - return sortedDataset.map(calculateRowInPercentage) + +function parseSort(sort: Sort | undefined): TransformedSort[] { + if (!sort) return [] + if (Array.isArray(sort)) return sort.map(completeSortItem) + + return [completeSortItem(sort)] } -export function getDataset(props: Props): DatasetOption { +export function getDataset(props: Props): { datasets: DatasetOption[], datasetIndex: number } { + const { fields, source } = props.dataset + const sort = parseSort(props.sort) + const dataset = { + dimensions: fields, + source: normalizeValues(source, props.normalizeValues), + } + + if (!sort.length) return { datasets: [dataset], datasetIndex: 0 } + + const datasets = [ + dataset, + { transform: { type: 'sort', config: sort } } + ] + return { - sourceHeader: true, - source: [props.dataset.fields].concat( - calculateDatasetSource(props).map((row) => row.map(String)), - ), + datasets, + datasetIndex: datasets.length - 1, } } diff --git a/packages/client/core/src/theme/ui/chart/config/funnel/index.ts b/packages/client/core/src/theme/ui/chart/config/funnel/index.ts index d21ac6625..b3dd47da3 100644 --- a/packages/client/core/src/theme/ui/chart/config/funnel/index.ts +++ b/packages/client/core/src/theme/ui/chart/config/funnel/index.ts @@ -4,6 +4,7 @@ import { getDataset } from '../common/getDataset' import setLegend from '../common/setLegend' import { COLORS, FONT } from '../common/designTokens' import { THEMES } from '../../themes' +import { AnimationEasing } from '../cartesian' const valuesRange = (values: number[]): number[] => { return values.reduce( @@ -24,11 +25,12 @@ const valuesRange = (values: number[]): number[] => { } type FunnelDirection = 'ascending' | 'descending' +type FunnelOrientation = 'vertical' | 'horizontal' const getColoredData = ({ data, valuesIndex, - sort, + sort = 'descending', visualMapColor, }: { data: DBSource @@ -79,23 +81,29 @@ const getColoredData = ({ export type FunnelChartProps = { dataset: Dataset sort?: FunnelDirection + orientation?: FunnelOrientation showColorGradient?: boolean - animation?: boolean showDecal?: boolean showLegend?: boolean showLabels?: boolean + animation?: boolean + animationEasing?: AnimationEasing + animationEasingUpdate?: AnimationEasing } export default function generateFunnelConfig({ dataset, - animation = false, + animation = true, + animationEasing = 'cubicInOut', + animationEasingUpdate = 'cubicInOut', sort = 'descending', + orientation = 'vertical', showColorGradient = false, showLabels = true, showDecal = false, showLegend = false, }: FunnelChartProps): EChartsOption { - const generatedDataset = getDataset({ dataset }) + const { datasets } = getDataset({ dataset }) const legend = setLegend({ show: showLegend }) const data = showColorGradient ? getColoredData({ @@ -109,13 +117,17 @@ export default function generateFunnelConfig({ return { animation, - dataset: [generatedDataset], + animationEasing, + animationEasingUpdate, + dataset: datasets, title: { show: false }, series: [ { type: 'funnel', data, sort, + orient: orientation, + funnelAlign: 'center', left: 0, width: '100%', top: showLegend ? 30 : 0, @@ -128,7 +140,7 @@ export default function generateFunnelConfig({ minMargin: 5, distance: 10, lineHeight: FONT.sizes.h6.lineHeight, - formatter: '{b}\n{formattedValue|{@1}}', + formatter: '{b}\n{formattedValue|{@0}}', rich: { formattedValue: { fontSize: FONT.sizes.h6.fontSize, diff --git a/packages/client/core/src/theme/ui/chart/config/index.ts b/packages/client/core/src/theme/ui/chart/config/index.ts index c28cae686..b167731fe 100644 --- a/packages/client/core/src/theme/ui/chart/config/index.ts +++ b/packages/client/core/src/theme/ui/chart/config/index.ts @@ -5,3 +5,4 @@ export { default as generateAreaConfig } from './area' export { default as generatePieConfig } from './pie' export { default as generateScatterConfig } from './scatter' export { default as generateFunnelConfig } from './funnel' + diff --git a/packages/client/core/src/theme/ui/chart/config/pie/index.ts b/packages/client/core/src/theme/ui/chart/config/pie/index.ts index 638aa4aa0..b6fca953e 100644 --- a/packages/client/core/src/theme/ui/chart/config/pie/index.ts +++ b/packages/client/core/src/theme/ui/chart/config/pie/index.ts @@ -1,7 +1,7 @@ import { EChartsOption } from 'echarts' import { isNaN } from 'lodash-es' import { DBSource, DBSourceRow, Dataset } from '../../types' -import { getDataset } from '../common/getDataset' +import { Sort, getDataset } from '../common/getDataset' import setLegend from '../common/setLegend' import { COLORS, FONT } from '../common/designTokens' @@ -37,12 +37,14 @@ const CONIFG_DEFAULTS: ConfigProps = { export type PieChartProps = { dataset: Dataset + sort?: Sort displayName?: string animation?: boolean config?: ConfigProps } export default function generatePieConfig({ dataset, + sort, displayName, animation = true, config: { @@ -53,12 +55,12 @@ export default function generatePieConfig({ showHole = false, } = CONIFG_DEFAULTS, }: PieChartProps): EChartsOption { - const generatedDataset = getDataset({ dataset }) + const { datasets, datasetIndex } = getDataset({ dataset, sort }) const legend = setLegend({ show: showLegend }) const totalValues = sumValues(dataset.source, DEFAULT_VALUE_INDEX) return { animation, - dataset: [generatedDataset], + dataset: datasets, title: { show: showTotalValue && showHole, left: 'center', @@ -111,14 +113,15 @@ export default function generatePieConfig({ }, labelLine: { show: true, - length: 40, - length2: 10, + length: 4, + length2: 1, maxSurfaceAngle: 100, }, encode: { value: DEFAULT_VALUE_INDEX, itemName: DEFAULT_LABEL_INDEX, }, + datasetIndex }, ], aria: { enabled: showDecal, decal: { show: showDecal } }, diff --git a/packages/client/core/src/theme/ui/chart/index.ts b/packages/client/core/src/theme/ui/chart/index.ts index 2596498e8..5aadefb74 100644 --- a/packages/client/core/src/theme/ui/chart/index.ts +++ b/packages/client/core/src/theme/ui/chart/index.ts @@ -3,3 +3,16 @@ export * from './config' import type { Dataset, DBSource } from './types' export type { Dataset, DBSource } + +export function blankSlateCssRoot(_: { loading: boolean }) { + return 'relative h-full w-full px-4' +} + +export function blankSlateCssContent() { + return "animate-gradient absolute inset-0 h-full w-full bg-gradient-to-r from-transparent via-white to-transparent" +} + +export const ERROR_CLASS = { + wrapper: 'w-full h-full flex', + content: 'h-full', +} diff --git a/packages/client/core/src/theme/ui/chart/types.ts b/packages/client/core/src/theme/ui/chart/types.ts index 2694a609a..61a55dfd7 100644 --- a/packages/client/core/src/theme/ui/chart/types.ts +++ b/packages/client/core/src/theme/ui/chart/types.ts @@ -7,7 +7,8 @@ export enum ChartLayerType { funnel = 'funnel', } -export type DBSourceRow = (string | number)[] +export type DBSourceColumn = string | number +export type DBSourceRow = DBSourceColumn[] export type DBSource = DBSourceRow[] export type EChartsSupportedType = | ChartLayerType.line diff --git a/packages/client/svelte/src/lib/internal/ui/chart/AreaChart.stories.svelte b/packages/client/svelte/src/lib/internal/ui/chart/AreaChart.stories.svelte index 40e76003c..45cec5e93 100644 --- a/packages/client/svelte/src/lib/internal/ui/chart/AreaChart.stories.svelte +++ b/packages/client/svelte/src/lib/internal/ui/chart/AreaChart.stories.svelte @@ -42,6 +42,7 @@