diff --git a/src/BaseCharts.js b/src/BaseCharts.ts similarity index 51% rename from src/BaseCharts.js rename to src/BaseCharts.ts index fc7f82ce..69d71cfd 100644 --- a/src/BaseCharts.js +++ b/src/BaseCharts.ts @@ -10,16 +10,25 @@ import { ScatterController } from 'chart.js' +import type { + ChartType, + ChartComponentLike, + DefaultDataPoint, + PluginOptionsByType, + ChartOptions +} from 'chart.js' + import { defineComponent, ref, + shallowRef, h, onMounted, onBeforeUnmount, watch, - computed, isProxy, - toRaw + toRaw, + PropType } from 'vue' import { @@ -29,27 +38,40 @@ import { getChartOptions, getChartData, setChartLabels, - setChartXLabels, - setChartYLabels, setChartDatasets, compareData -} from './utils.js' - -export const generateChart = (chartId, chartType, chartController) => +} from './utils' + +import type { + TChartData, + TChartOptions, + TypedChartJS, + TypedChartComponent +} from './types' + +export const generateChart = < + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + chartId: string, + chartType: TType, + chartController: ChartComponentLike +): TypedChartComponent => defineComponent({ props: { chartData: { - type: Object, + type: Object as PropType>, required: true }, + chartOptions: { + type: Object as PropType>, + default: () => {} + }, datasetIdKey: { type: String, default: 'label' }, - chartOptions: { - type: Object, - default: () => {} - }, chartId: { type: String, default: chartId @@ -67,87 +89,106 @@ export const generateChart = (chartId, chartType, chartController) => default: '' }, styles: { - type: Object, + type: Object as PropType>, default: () => {} }, plugins: { - type: Object, + type: Object as PropType>, default: () => {} } }, setup(props, context) { ChartJS.register(chartController) - const _chart = ref(null) - const canvasEl = ref(null) + const _chart = shallowRef | null>(null) + const canvasEl = ref(null) - const hasChart = computed(() => _chart.value !== null) - - function renderChart(data, options) { - if (hasChart.value) { - chartDestroy(toRaw(_chart.value), context) + function renderChart( + data: TChartData, + options: TChartOptions + ): void { + if (_chart.value !== null) { + chartDestroy(toRaw(_chart.value), context) } if (canvasEl.value === null) { throw new Error( 'Please remove the tags from your chart component. See https://vue-chartjs.org/guide/#vue-single-file-components' ) + } else { + const chartData = getChartData( + data, + props.datasetIdKey + ) + const canvasEl2DContext = canvasEl.value.getContext('2d') + + if (canvasEl2DContext !== null) { + _chart.value = new ChartJS( + canvasEl2DContext, + { + type: chartType, + data: isProxy(data) ? new Proxy(chartData, {}) : chartData, + options: getChartOptions(options, props.plugins) + } + ) + } } - - const chartData = getChartData(data) - - _chart.value = new ChartJS(canvasEl.value.getContext('2d'), { - type: chartType, - data: isProxy(data) ? new Proxy(chartData, {}) : chartData, - options: getChartOptions(options, props.plugins) - }) } - function chartDataHandler(newValue, oldValue) { + function chartDataHandler( + newValue: TChartData, + oldValue: TChartData + ): void { const newData = isProxy(newValue) ? toRaw(newValue) : { ...newValue } const oldData = isProxy(oldValue) ? toRaw(oldValue) : { ...oldValue } if (Object.keys(oldData).length > 0) { const chart = toRaw(_chart.value) - const isEqualLabelsAndDatasetsLength = compareData(newData, oldData) - - if (isEqualLabelsAndDatasetsLength) { - setChartDatasets(chart.data, newData, props.datasetIdKey) - - if (Object.prototype.hasOwnProperty.call(newData, 'labels')) { - setChartLabels(chart, newData.labels, context) - } - - if (Object.prototype.hasOwnProperty.call(newData, 'xLabels')) { - setChartXLabels(chart, newData.xLabels, context) - } + const isEqualLabelsAndDatasetsLength = compareData< + TType, + TData, + TLabel + >(newData, oldData) + + if (isEqualLabelsAndDatasetsLength && chart !== null) { + setChartDatasets( + chart?.data, + newData, + props.datasetIdKey + ) - if (Object.prototype.hasOwnProperty.call(newData, 'yLabels')) { - setChartYLabels(chart, newData.yLabels, context) + if (newData.labels !== undefined) { + setChartLabels( + chart, + newData.labels, + context + ) } - chartUpdate(chart, context) + chartUpdate(chart, context) } else { - if (hasChart.value) { - chartDestroy(chart, context) + if (chart !== null) { + chartDestroy(chart, context) } - chartCreate( + chartCreate( renderChart, - [props.chartData, props.chartOptions], - context + context, + props.chartData, + props.chartOptions as ChartOptions ) } } else { - if (hasChart.value) { - chartDestroy(toRaw(_chart.value), context) + if (_chart.value !== null) { + chartDestroy(toRaw(_chart.value), context) } - chartCreate( + chartCreate( renderChart, - [props.chartData, props.chartOptions], - context + context, + props.chartData, + props.chartOptions as ChartOptions ) } } @@ -163,16 +204,17 @@ export const generateChart = (chartId, chartType, chartController) => 'datasets' in props.chartData && props.chartData.datasets.length > 0 ) { - chartCreate( + chartCreate( renderChart, - [props.chartData, props.chartOptions], - context + context, + props.chartData, + props.chartOptions as ChartOptions ) } }) onBeforeUnmount(() => { - if (hasChart.value) { + if (_chart.value !== null) { chartDestroy(toRaw(_chart.value), context) } }) @@ -187,7 +229,7 @@ export const generateChart = (chartId, chartType, chartController) => }) ]) } - }) + }) as any export const Bar = /* #__PURE__ */ generateChart( 'bar-chart', diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..e0671020 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,91 @@ +import { Chart as ChartJS } from 'chart.js' +import type { + ChartType, + ChartData, + ChartOptions, + PluginChartOptions, + DefaultDataPoint, + PluginOptionsByType +} from 'chart.js' + +import { + ComponentOptionsMixin, + ComputedOptions, + DefineComponent, + MethodOptions, + Ref, + ShallowRef +} from 'vue' + +import { ChartEmits } from './utils' + +export type TChartData< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> = ChartData + +export type TChartOptions = ChartOptions + +export type TChartPlugins = PluginChartOptions + +export type TypedChartJS< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> = ChartJS + +export interface IChartProps< + TType extends ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + chartData: TChartData + datasetIdKey: string + chartOptions: TChartOptions + chartId: string + width: number + height: number + cssClasses: string + styles: Partial + plugins: PluginOptionsByType +} + +export interface TypedChartComponentData< + TType extends ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + _chart: ShallowRef | null> + canvasEl: Ref + renderChart: ( + data: TChartData, + options: TChartOptions + ) => void + chartDataHandler: ( + newValue: TChartData, + oldValue: TChartData + ) => void +} + +export type TypedChartEmits = { + [ChartEmits.ChartRendered]: () => true + [ChartEmits.ChartUpdated]: () => true + [ChartEmits.ChartDestroyed]: () => true + [ChartEmits.LabelsUpdated]: () => true +} + +export type TypedChartComponent< + TType extends ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> = DefineComponent< + IChartProps, + TypedChartComponentData, + unknown, + ComputedOptions, + MethodOptions, + ComponentOptionsMixin, + ComponentOptionsMixin, + TypedChartEmits +> diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index f10a2b32..00000000 --- a/src/utils.js +++ /dev/null @@ -1,96 +0,0 @@ -export function chartCreate( - createChartFunction, - createChartFunctionArgs, - context -) { - createChartFunction(...createChartFunctionArgs) - context.emit('chart:rendered') -} - -export function chartUpdate(chart, context) { - chart.update() - context.emit('chart:updated') -} - -export function chartDestroy(chart, context) { - chart.destroy() - context.emit('chart:destroyed') -} - -export function getChartData(data, datasetIdKey) { - const nextData = { - labels: typeof data.labels === 'undefined' ? [] : [...data.labels], - datasets: [] - } - - setChartDatasets(nextData, { ...data }, datasetIdKey) - return nextData -} - -export function getChartOptions(options, plugins) { - const chartOptions = options - - if (typeof plugins !== 'undefined' && Object.keys(plugins).length > 0) { - chartOptions.plugins = { ...chartOptions.plugins, ...plugins } - } - - return chartOptions -} - -export function setChartDatasets(oldData, newData, datasetIdKey) { - const addedDatasets = [] - - oldData.datasets = newData.datasets.map(nextDataset => { - // given the new set, find it's current match - const currentDataset = oldData.datasets.find( - dataset => dataset[datasetIdKey] === nextDataset[datasetIdKey] - ) - - // There is no original to update, so simply add new one - if ( - !currentDataset || - !nextDataset.data || - addedDatasets.includes(currentDataset) - ) { - return { ...nextDataset } - } - - addedDatasets.push(currentDataset) - - Object.assign(currentDataset, nextDataset) - - return currentDataset - }) -} - -export function setChartLabels(chart, labels, context) { - chart.data.labels = labels - context.emit('labels:updated') -} - -export function setChartXLabels(chart, xLabels, context) { - chart.data.xLabels = xLabels - context.emit('xlabels:updated') -} - -export function setChartYLabels(chart, yLabels, context) { - chart.data.yLabels = yLabels - context.emit('ylabels:updated') -} - -export function compareData(newData, oldData) { - // Get new and old DataSet Labels - const newDatasetLabels = newData.datasets.map(dataset => { - return dataset.label - }) - - const oldDatasetLabels = oldData.datasets.map(dataset => { - return dataset.label - }) - - // Check if Labels are equal and if dataset length is equal - return ( - oldData.datasets.length === newData.datasets.length && - newDatasetLabels.every((value, index) => value === oldDatasetLabels[index]) - ) -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..5a9e2137 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,164 @@ +import type { + ChartType, + ChartDataset, + DefaultDataPoint, + PluginOptionsByType +} from 'chart.js' + +import type { TChartData, TChartOptions, TypedChartJS } from './types' + +import { SetupContext } from 'vue' + +export enum ChartEmits { + ChartRendered = 'chart:rendered', + ChartUpdated = 'chart:updated', + ChartDestroyed = 'chart:destroyed', + LabelsUpdated = 'labels:updated' +} + +export function chartCreate< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + createChartFunction: ( + data: TChartData, + options: TChartOptions + ) => void, + context: SetupContext, + chartData: TChartData, + chartOptions: TChartOptions +): void { + createChartFunction(chartData, chartOptions) + context.emit(ChartEmits.ChartRendered) +} + +export function chartUpdate< + TType extends ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>(chart: TypedChartJS, context: SetupContext): void { + chart.update() + context.emit(ChartEmits.ChartUpdated) +} + +export function chartDestroy< + TType extends ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>(chart: TypedChartJS, context: SetupContext): void { + chart.destroy() + context.emit(ChartEmits.ChartDestroyed) +} + +export function getChartData< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + data: TChartData, + datasetIdKey: string +): TChartData { + const nextData = { + labels: typeof data.labels === 'undefined' ? [] : [...data.labels], + datasets: [] + } + + setChartDatasets(nextData, { ...data }, datasetIdKey) + return nextData +} + +export function getChartOptions( + options?: TChartOptions, + plugins?: PluginOptionsByType +): TChartOptions | undefined { + const chartOptions = options + + if ( + chartOptions !== undefined && + 'plugins' in chartOptions && + typeof plugins !== 'undefined' && + Object.keys(plugins).length > 0 + ) { + chartOptions.plugins = { + ...chartOptions.plugins, + ...plugins + } + } + + return chartOptions +} + +export function setChartDatasets< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + oldData: TChartData, + newData: TChartData, + datasetIdKey: string +): void { + const addedDatasets: ChartDataset[] = [] + + oldData.datasets = newData.datasets.map( + (nextDataset: Record) => { + // given the new set, find it's current match + const currentDataset = oldData.datasets.find( + (dataset: Record) => + dataset[datasetIdKey] === nextDataset[datasetIdKey] + ) + + // There is no original to update, so simply add new one + if ( + !currentDataset || + !nextDataset.data || + addedDatasets.includes(currentDataset) + ) { + return { ...nextDataset } + } + + addedDatasets.push(currentDataset) + + Object.assign(currentDataset, nextDataset) + + return currentDataset + } + ) as ChartDataset[] +} + +export function setChartLabels< + TType extends ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + chart: TypedChartJS, + labels: TLabel[], + context: SetupContext +): void { + chart.data.labels = labels + context.emit(ChartEmits.LabelsUpdated) +} + +export function compareData< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +>( + newData: TChartData, + oldData: TChartData +): boolean { + // Get new and old DataSet Labels + const newDatasetLabels = newData.datasets.map(dataset => { + return dataset.label + }) + + const oldDatasetLabels = oldData.datasets.map(dataset => { + return dataset.label + }) + + // Check if Labels are equal and if dataset length is equal + return ( + oldData.datasets.length === newData.datasets.length && + newDatasetLabels.every((value, index) => value === oldDatasetLabels[index]) + ) +}