diff --git a/packages/ag-charts-community/src/chart/agChart.test.ts b/packages/ag-charts-community/src/chart/agChart.test.ts index e49c1e5a09..fcf286cacc 100644 --- a/packages/ag-charts-community/src/chart/agChart.test.ts +++ b/packages/ag-charts-community/src/chart/agChart.test.ts @@ -152,7 +152,7 @@ describe('update', () => { expect(chart.subtitle?.enabled).toBe(false); expect((chart as any).background.fill).toBe('red'); expect((chart as any).background.visible).toBe(false); - expect((chart.series[0] as any).marker.shape).toBe('plus'); + expect((chart.series[0] as any).properties.marker.shape).toBe('plus'); AgCharts.updateDelta(chartProxy, { data: revenueProfitData, @@ -254,15 +254,15 @@ describe('update', () => { expect(updatedSeries.length).toEqual(4); expect(updatedSeries[0]).not.toBe(createdSeries[0]); expect(updatedSeries[1]).not.toBe(createdSeries[1]); - expect((updatedSeries[0] as any).marker.shape).toEqual('square'); - expect((updatedSeries[0] as any).marker.size).toEqual(10); - expect((updatedSeries[1] as any).fill).toEqual('lime'); - expect((updatedSeries[1] as any).yKey).toEqual('profit'); - expect((updatedSeries[2] as any).fill).toEqual('cyan'); - expect((updatedSeries[2] as any).yKey).toEqual('foobar'); + expect((updatedSeries[0].properties as any).marker.shape).toEqual('square'); + expect((updatedSeries[0].properties as any).marker.size).toEqual(10); + expect((updatedSeries[1].properties as any).fill).toEqual('lime'); + expect((updatedSeries[1].properties as any).yKey).toEqual('profit'); + expect((updatedSeries[2].properties as any).fill).toEqual('cyan'); + expect((updatedSeries[2].properties as any).yKey).toEqual('foobar'); expect(updatedSeries[3]).toBeInstanceOf(AreaSeries); - expect((updatedSeries[3] as any).xKey).toEqual('month'); - expect((updatedSeries[3] as any).yKey).toEqual('bazqux'); + expect((updatedSeries[3].properties as any).xKey).toEqual('month'); + expect((updatedSeries[3].properties as any).yKey).toEqual('bazqux'); AgCharts.update(chartProxy, { data: revenueProfitData, @@ -336,10 +336,10 @@ describe('update', () => { expect(updatedSeries3[0]).toBeInstanceOf(BarSeries); expect(updatedSeries3[1]).toBeInstanceOf(BarSeries); expect(updatedSeries3[2]).toBeInstanceOf(LineSeries); - expect((updatedSeries3[0] as any).yKey).toEqual('profit'); - expect((updatedSeries3[1] as any).yKey).toEqual('foobar'); - expect((updatedSeries3[2] as any).yKey).toEqual('revenue'); - expect((updatedSeries3[2] as any).marker.size).toEqual(10); + expect((updatedSeries3[0].properties as any).yKey).toEqual('profit'); + expect((updatedSeries3[1].properties as any).yKey).toEqual('foobar'); + expect((updatedSeries3[2].properties as any).yKey).toEqual('revenue'); + expect((updatedSeries3[2].properties as any).marker.size).toEqual(10); const lineSeries = updatedSeries3[1]; diff --git a/packages/ag-charts-community/src/chart/agChartV2.ts b/packages/ag-charts-community/src/chart/agChartV2.ts index a6659ff996..bcc334fdda 100644 --- a/packages/ag-charts-community/src/chart/agChartV2.ts +++ b/packages/ag-charts-community/src/chart/agChartV2.ts @@ -43,8 +43,8 @@ import { optionsType, } from './mapping/types'; import { PolarChart } from './polarChart'; -import { PieTitle } from './series/polar/pieSeries'; import type { Series } from './series/series'; +import type { SeriesGrouping } from './series/seriesStateManager'; const debug = Debug.create(true, 'opts'); @@ -484,18 +484,18 @@ function applySeries(chart: Chart, options: AgChartOptions) { return false; } - const keysToConsider = ['type', 'direction', 'xKey', 'yKey', 'sizeKey', 'angleKey', 'stacked', 'stackGroup']; + const keysToConsider = ['direction', 'xKey', 'yKey', 'sizeKey', 'angleKey', 'stacked', 'stackGroup']; let matchingTypes = chart.series.length === optSeries.length; for (let i = 0; i < chart.series.length && matchingTypes; i++) { + matchingTypes &&= chart.series[i].type === (optSeries[i] as any).type; for (const key of keysToConsider) { - matchingTypes &&= (chart.series[i] as any)[key] === (optSeries[i] as any)[key]; + matchingTypes &&= (chart.series[i].properties as any)[key] === (optSeries[i] as any)[key]; } } // Try to optimise series updates if series count and types didn't change. if (matchingTypes) { - const moduleContext = chart.getModuleContext(); chart.series.forEach((s, i) => { const previousOpts = chart.processedOptions?.series?.[i] ?? {}; const seriesDiff = jsonDiff(previousOpts, optSeries[i] ?? {}); @@ -506,7 +506,7 @@ function applySeries(chart: Chart, options: AgChartOptions) { debug(`AgChartV2.applySeries() - applying series diff idx ${i}`, seriesDiff); - applySeriesValues(s as any, moduleContext, seriesDiff, { path: `series[${i}]`, index: i }); + applySeriesValues(s as any, seriesDiff); s.markNodeDataDirty(); }); @@ -556,16 +556,14 @@ function createSeries(chart: Chart, options: SeriesOptionsTypes[]): Series[ const series: Series[] = []; const moduleContext = chart.getModuleContext(); - let index = 0; for (const seriesOptions of options ?? []) { - const path = `series[${index++}]`; const type = seriesOptions.type ?? 'unknown'; if (isEnterpriseSeriesType(type) && !isEnterpriseSeriesTypeLoaded(type)) { continue; } const seriesInstance = getSeries(type, moduleContext); applySeriesOptionModules(seriesInstance, seriesOptions); - applySeriesValues(seriesInstance, moduleContext, seriesOptions, { path, index }); + applySeriesValues(seriesInstance, seriesOptions); series.push(seriesInstance); } @@ -650,53 +648,23 @@ function applyOptionValues( return jsonApply(target, options, applyOpts); } -function applySeriesValues( - target: Series, - moduleContext: ModuleContext, - options?: AgBaseSeriesOptions, - { path, index }: { path?: string; index?: number } = {} -): Series { - const skip: string[] = ['series[].listeners', 'series[].seriesGrouping']; - const jsonApplyOptions = getJsonApplyOptions(moduleContext); - const ctrs = jsonApplyOptions.constructors ?? {}; - // Allow context to be injected and meet the type requirements - class PieTitleWithContext extends PieTitle { - constructor() { - super(moduleContext); - } +function applySeriesValues(target: Series, options: AgBaseSeriesOptions): Series { + target.properties.set(options); + target.data = options.data; + if ('errorBar' in target && 'errorBar' in options) { + (target.errorBar as any).properties.set(options.errorBar); } - const seriesTypeOverrides = { - constructors: { - ...ctrs, - title: target.type === 'pie' ? PieTitleWithContext : ctrs['title'], - }, - }; - - const applyOpts = { - ...jsonApplyOptions, - ...seriesTypeOverrides, - skip: ['series[].type', ...(skip ?? [])], - ...(path ? { path } : {}), - idx: index ?? -1, - }; - - const result = jsonApply(target, options, applyOpts); if (options?.listeners != null) { registerListeners(target, options.listeners); } - const { seriesGrouping } = options as any; - if ('seriesGrouping' in (options ?? {})) { - if (seriesGrouping) { - target.seriesGrouping = Object.freeze({ - ...(target.seriesGrouping ?? {}), - ...seriesGrouping, - }); - } else { - target.seriesGrouping = seriesGrouping; - } + if ('seriesGrouping' in options) { + const seriesGrouping = options.seriesGrouping as SeriesGrouping | undefined; + target.seriesGrouping = seriesGrouping + ? Object.freeze({ ...target.seriesGrouping, ...seriesGrouping }) + : undefined; } - return result; + return target; } diff --git a/packages/ag-charts-community/src/chart/axis/axis.ts b/packages/ag-charts-community/src/chart/axis/axis.ts index 131611b2b8..8046a63291 100644 --- a/packages/ag-charts-community/src/chart/axis/axis.ts +++ b/packages/ag-charts-community/src/chart/axis/axis.ts @@ -259,6 +259,7 @@ export abstract class Axis> = Scale> = Scale typeof title == 'object', 'Title object'), { optional: true }) public title?: AxisTitle = undefined; - protected _titleCaption = new Caption(this.moduleCtx); + protected _titleCaption = new Caption(); private setDomain() { const { @@ -712,7 +713,7 @@ export abstract class Axis> = Scale string = undefined; + formatter?: (params: AgAxisCaptionFormatterParams) => string; } diff --git a/packages/ag-charts-community/src/chart/caption.ts b/packages/ag-charts-community/src/chart/caption.ts index 22b31b99f1..7893fc3b7d 100644 --- a/packages/ag-charts-community/src/chart/caption.ts +++ b/packages/ag-charts-community/src/chart/caption.ts @@ -3,6 +3,7 @@ import type { FontStyle, FontWeight, TextWrap } from '../options/agChartOptions' import { PointerEvents } from '../scene/node'; import { Text } from '../scene/shape/text'; import { createId } from '../util/id'; +import { BaseProperties } from '../util/properties'; import { ProxyPropertyOnWrite } from '../util/proxy'; import { BOOLEAN, @@ -17,27 +18,30 @@ import { import type { InteractionEvent } from './interaction/interactionManager'; import { toTooltipHtml } from './tooltip/tooltip'; -export class Caption { +export class Caption extends BaseProperties { static readonly SMALL_PADDING = 10; static readonly LARGE_PADDING = 20; readonly id = createId(this); - readonly node: Text = new Text(); + readonly node = new Text().setProperties({ + textAlign: 'center', + pointerEvents: PointerEvents.None, + }); @Validate(BOOLEAN) enabled = false; @Validate(STRING, { optional: true }) @ProxyPropertyOnWrite('node') - text?: string = undefined; + text?: string; @Validate(FONT_STYLE, { optional: true }) @ProxyPropertyOnWrite('node') - fontStyle: FontStyle | undefined; + fontStyle?: FontStyle; @Validate(FONT_WEIGHT, { optional: true }) @ProxyPropertyOnWrite('node') - fontWeight: FontWeight | undefined; + fontWeight?: FontWeight; @Validate(POSITIVE_NUMBER) @ProxyPropertyOnWrite('node') @@ -49,7 +53,7 @@ export class Caption { @Validate(COLOR_STRING, { optional: true }) @ProxyPropertyOnWrite('node', 'fill') - color: string | undefined; + color?: string; @Validate(POSITIVE_NUMBER, { optional: true }) spacing?: number; @@ -58,24 +62,18 @@ export class Caption { lineHeight?: number; @Validate(POSITIVE_NUMBER, { optional: true }) - maxWidth?: number = undefined; + maxWidth?: number; @Validate(POSITIVE_NUMBER, { optional: true }) - maxHeight?: number = undefined; + maxHeight?: number; @Validate(TEXT_WRAP) wrapping: TextWrap = 'always'; - private truncated: boolean = false; + private truncated = false; - private destroyFns: Function[] = []; - - constructor(protected readonly moduleCtx: ModuleContext) { - const node = this.node; - node.textAlign = 'center'; - node.pointerEvents = PointerEvents.None; - - this.destroyFns.push(moduleCtx.interactionManager.addListener('hover', (e) => this.handleMouseMove(e))); + registerInteraction(moduleCtx: ModuleContext) { + return moduleCtx.interactionManager.addListener('hover', (event) => this.handleMouseMove(moduleCtx, event)); } computeTextWrap(containerWidth: number, containerHeight: number) { @@ -91,9 +89,8 @@ export class Caption { this.truncated = truncated; } - handleMouseMove(event: InteractionEvent<'hover'>) { - const { enabled } = this; - if (!enabled) { + handleMouseMove(moduleCtx: ModuleContext, event: InteractionEvent<'hover'>) { + if (!this.enabled) { return; } @@ -102,7 +99,7 @@ export class Caption { const pointerInsideCaption = this.node.visible && bbox.containsPoint(offsetX, offsetY); if (!pointerInsideCaption) { - this.moduleCtx.tooltipManager.removeTooltip(this.id); + moduleCtx.tooltipManager.removeTooltip(this.id); return; } @@ -111,11 +108,11 @@ export class Caption { event.consume(); if (!this.truncated) { - this.moduleCtx.tooltipManager.removeTooltip(this.id); + moduleCtx.tooltipManager.removeTooltip(this.id); return; } - this.moduleCtx.tooltipManager.updateTooltip( + moduleCtx.tooltipManager.updateTooltip( this.id, { pageX, pageY, offsetX, offsetY, event, showArrow: false, addCustomClass: false }, toTooltipHtml({ content: this.text }) diff --git a/packages/ag-charts-community/src/chart/cartesianChart.test.ts b/packages/ag-charts-community/src/chart/cartesianChart.test.ts index 952bb5b7a3..b8be758b20 100644 --- a/packages/ag-charts-community/src/chart/cartesianChart.test.ts +++ b/packages/ag-charts-community/src/chart/cartesianChart.test.ts @@ -223,7 +223,7 @@ describe('CartesianChart', () => { await waitForChartStability(chart); const seriesImpl = chart.series.find( - (v: any) => v.yKey === yKey || v.yKeys?.some((s) => s.includes(yKey)) + (v: any) => v.properties.yKey === yKey || v.properties.yKeys?.some((s) => s.includes(yKey)) ); if (seriesImpl == null) fail('No seriesImpl found'); diff --git a/packages/ag-charts-community/src/chart/chart.test.ts b/packages/ag-charts-community/src/chart/chart.test.ts index 953e046a15..8285f4c81c 100644 --- a/packages/ag-charts-community/src/chart/chart.test.ts +++ b/packages/ag-charts-community/src/chart/chart.test.ts @@ -236,8 +236,8 @@ describe('Chart', () => { }, getNodePoint: (item) => [item.point.x, item.point.y], getDatumValues: (item, series) => { - const xValue = item.datum[series['xKey']]; - const yValue = item.datum[series['yKey']]; + const xValue = item.datum[series.properties['xKey']]; + const yValue = item.datum[series.properties['yKey']]; return [xValue, yValue]; }, }); @@ -257,8 +257,8 @@ describe('Chart', () => { }, getNodePoint: (item) => [item.point.x, item.point.y], getDatumValues: (item, series) => { - const xValue = item.datum[series.xKey]; - const yValue = item.datum[series.yKey]; + const xValue = item.datum[series.properties.xKey]; + const yValue = item.datum[series.properties.yKey]; return [xValue, yValue]; }, }); @@ -281,8 +281,8 @@ describe('Chart', () => { }, getNodePoint: (item) => [item.point.x, item.point.y], getDatumValues: (item, series) => { - const xValue = item.datum[series['xKey']]; - const yValue = item.datum[series['yKey']]; + const xValue = item.datum[series.properties['xKey']]; + const yValue = item.datum[series.properties['yKey']]; return [xValue, yValue]; }, }); @@ -299,8 +299,8 @@ describe('Chart', () => { }, getNodePoint: (item) => [item.x + item.width / 2, item.y + item.height / 2], getDatumValues: (item, series) => { - const xValue = item.datum[series.xKey]; - const yValue = item.datum[series.yKey]; + const xValue = item.datum[series.properties.xKey]; + const yValue = item.datum[series.properties.yKey]; return [xValue, yValue]; }, }); @@ -317,8 +317,8 @@ describe('Chart', () => { getNodeData: (series) => series.sectorLabelSelection.nodes(), getNodePoint: (item) => [item.x, item.y], getDatumValues: (item, series) => { - const category = item.datum.datum[series.sectorLabelKey]; - const value = item.datum.datum[series.angleKey]; + const category = item.datum.datum[series.properties.sectorLabelKey]; + const value = item.datum.datum[series.properties.angleKey]; return [category, value]; }, getTooltipRenderedValues: (params) => [params.datum[params.sectorLabelKey], params.datum[params.angleKey]], diff --git a/packages/ag-charts-community/src/chart/chart.ts b/packages/ag-charts-community/src/chart/chart.ts index b16a9227ae..5787268ad0 100644 --- a/packages/ag-charts-community/src/chart/chart.ts +++ b/packages/ag-charts-community/src/chart/chart.ts @@ -979,7 +979,9 @@ export abstract class Chart extends Observable implements AgChartInstance { const isCategoryLegendData = ( data: Array ): data is CategoryLegendDatum[] => data.every((d) => d.legendType === 'category'); - const legendData = this.series.filter((s) => s.showInLegend).flatMap((s) => s.getLegendData(legendType)); + const legendData = this.series + .filter((s) => s.properties.showInLegend) + .flatMap((s) => s.getLegendData(legendType)); if (isCategoryLegendData(legendData)) { this.validateCategoryLegendData(legendData); @@ -1168,7 +1170,7 @@ export abstract class Chart extends Observable implements AgChartInstance { } const isPixelRange = pixelRange != null; - const tooltipEnabled = this.tooltip.enabled && pick.series.tooltip.enabled; + const tooltipEnabled = this.tooltip.enabled && pick.series.properties.tooltip.enabled; const exactlyMatched = range === 'exact' && pick.distance === 0; const rangeMatched = range === 'nearest' || isPixelRange || exactlyMatched; const shouldUpdateTooltip = tooltipEnabled && rangeMatched && (!isNewDatum || html !== undefined); @@ -1239,7 +1241,7 @@ export abstract class Chart extends Observable implements AgChartInstance { const nearestNode = this.pickSeriesNode({ x: event.offsetX, y: event.offsetY }, false); const datum = nearestNode?.datum; - const nodeClickRange = datum?.series.nodeClickRange; + const nodeClickRange = datum?.series.properties.nodeClickRange; let pixelRange; if (typeof nodeClickRange === 'number' && Number.isFinite(nodeClickRange)) { @@ -1313,11 +1315,11 @@ export abstract class Chart extends Observable implements AgChartInstance { } // Adjust cursor if a specific datum is highlighted, rather than just a series. - if (lastSeries?.cursor && lastDatum) { + if (lastSeries?.properties.cursor && lastDatum) { this.cursorManager.updateCursor(lastSeries.id); } - if (newSeries?.cursor && newDatum) { - this.cursorManager.updateCursor(newSeries.id, newSeries.cursor); + if (newSeries?.properties.cursor && newDatum) { + this.cursorManager.updateCursor(newSeries.id, newSeries.properties.cursor); } this.lastPick = event.currentHighlight ? { datum: event.currentHighlight } : undefined; diff --git a/packages/ag-charts-community/src/chart/chartOptions.ts b/packages/ag-charts-community/src/chart/chartOptions.ts index 90596a6bcd..8d18c43e78 100644 --- a/packages/ag-charts-community/src/chart/chartOptions.ts +++ b/packages/ag-charts-community/src/chart/chartOptions.ts @@ -1,9 +1,8 @@ import type { ModuleContext } from '../module/moduleContext'; -import { DropShadow } from '../scene/dropShadow'; import type { JsonApplyParams } from '../util/json'; import { AxisTitle } from './axis/axisTitle'; import { Caption } from './caption'; -import { DoughnutInnerCircle, DoughnutInnerLabel } from './series/polar/pieSeries'; +import { DoughnutInnerCircle, DoughnutInnerLabel } from './series/polar/pieSeriesProperties'; export const JSON_APPLY_PLUGINS: JsonApplyParams = { constructors: {}, @@ -16,7 +15,6 @@ export function assignJsonApplyConstructedArray(array: any[], ctor: new () => an const JSON_APPLY_OPTIONS: JsonApplyParams = { constructors: { - shadow: DropShadow, innerCircle: DoughnutInnerCircle, 'axes[].title': AxisTitle, 'series[].innerLabels[]': DoughnutInnerLabel, @@ -32,7 +30,8 @@ export function getJsonApplyOptions(ctx: ModuleContext): JsonApplyParams { // Allow context to be injected and meet the type requirements class CaptionWithContext extends Caption { constructor() { - super(ctx); + super(); + this.registerInteraction(ctx); } } return { diff --git a/packages/ag-charts-community/src/chart/interaction/tooltipManager.ts b/packages/ag-charts-community/src/chart/interaction/tooltipManager.ts index 6c0bb6832b..b9f962d4e0 100644 --- a/packages/ag-charts-community/src/chart/interaction/tooltipManager.ts +++ b/packages/ag-charts-community/src/chart/interaction/tooltipManager.ts @@ -121,9 +121,10 @@ export class TooltipManager { window: Window ): TooltipMeta { const { pageX, pageY, offsetX, offsetY } = event; + const { tooltip } = datum.series.properties; const position = { - xOffset: datum.series.tooltip.position.xOffset, - yOffset: datum.series.tooltip.position.yOffset, + xOffset: tooltip.position.xOffset, + yOffset: tooltip.position.yOffset, }; const meta: TooltipMeta = { pageX, @@ -131,7 +132,7 @@ export class TooltipManager { offsetX, offsetY, event: event, - showArrow: datum.series.tooltip.showArrow, + showArrow: tooltip.showArrow, position, }; @@ -139,7 +140,7 @@ export class TooltipManager { // datum.midPoint. Using datum.yBar.upperPoint renders the tooltip higher up. const refPoint: Point | undefined = datum.yBar?.upperPoint ?? datum.midPoint; - if (datum.series.tooltip.position.type === 'node' && refPoint) { + if (tooltip.position.type === 'node' && refPoint) { const { x, y } = refPoint; const point = datum.series.contentGroup.inverseTransformPoint(x, y); const canvasRect = canvas.element.getBoundingClientRect(); @@ -151,7 +152,7 @@ export class TooltipManager { offsetY: Math.round(point.y), }; } - meta.enableInteraction = datum.series.tooltip.interaction?.enabled ?? false; + meta.enableInteraction = tooltip.interaction?.enabled ?? false; return meta; } diff --git a/packages/ag-charts-community/src/chart/label.ts b/packages/ag-charts-community/src/chart/label.ts index c25b60099a..e6744f8670 100644 --- a/packages/ag-charts-community/src/chart/label.ts +++ b/packages/ag-charts-community/src/chart/label.ts @@ -5,6 +5,7 @@ import type { Matrix } from '../scene/matrix'; import { getFont } from '../scene/shape/text'; import { normalizeAngle360, toRadians } from '../util/angle'; import type { PointLabelDatum } from '../util/labelPlacement'; +import { BaseProperties } from '../util/properties'; import type { RequireOptional } from '../util/types'; import { BOOLEAN, @@ -18,7 +19,10 @@ import { } from '../util/validation'; import type { ChartAxisLabelFlipFlag } from './chartAxis'; -export class Label implements AgChartLabelOptions> { +export class Label + extends BaseProperties + implements AgChartLabelOptions> +{ @Validate(BOOLEAN) enabled = true; @@ -32,10 +36,10 @@ export class Label implements AgChartLabelOptions fontWeight?: FontWeight; @Validate(POSITIVE_NUMBER) - fontSize = 12; + fontSize: number = 12; @Validate(STRING) - fontFamily = 'Verdana, sans-serif'; + fontFamily: string = 'Verdana, sans-serif'; @Validate(FUNCTION, { optional: true }) formatter?: (params: AgChartLabelFormatterParams & RequireOptional) => string | undefined; diff --git a/packages/ag-charts-community/src/chart/mapping/prepare.ts b/packages/ag-charts-community/src/chart/mapping/prepare.ts index 6fb4d75a5f..bdcec61f3a 100644 --- a/packages/ag-charts-community/src/chart/mapping/prepare.ts +++ b/packages/ag-charts-community/src/chart/mapping/prepare.ts @@ -442,9 +442,9 @@ function prepareEnabledOptions(options: T, mergedOptio function preparePieOptions(pieSeriesTheme: any, seriesOptions: any, mergedSeries: any) { if (Array.isArray(seriesOptions.innerLabels)) { - mergedSeries.innerLabels = seriesOptions.innerLabels.map((ln: any) => { - return jsonMerge([pieSeriesTheme.innerLabels, ln]); - }); + mergedSeries.innerLabels = seriesOptions.innerLabels.map((innerLabel: any) => + jsonMerge([pieSeriesTheme.innerLabels, innerLabel]) + ); } else { mergedSeries.innerLabels = DELETE; } diff --git a/packages/ag-charts-community/src/chart/series/cartesian/abstractBarSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/abstractBarSeries.ts index d1e797f147..5bb1de082c 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/abstractBarSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/abstractBarSeries.ts @@ -4,8 +4,13 @@ import { DIRECTION, Validate } from '../../../util/validation'; import type { ChartAxis } from '../../chartAxis'; import { ChartAxisDirection } from '../../chartAxisDirection'; import type { SeriesNodeDatum } from '../seriesTypes'; -import { CartesianSeries } from './cartesianSeries'; import type { CartesianSeriesNodeDataContext, CartesianSeriesNodeDatum } from './cartesianSeries'; +import { CartesianSeries, CartesianSeriesProperties } from './cartesianSeries'; + +export abstract class AbstractBarSeriesProperties extends CartesianSeriesProperties { + @Validate(DIRECTION) + direction: Direction = 'vertical'; +} export abstract class AbstractBarSeries< TNode extends Node, @@ -13,15 +18,18 @@ export abstract class AbstractBarSeries< TLabel extends SeriesNodeDatum = TDatum, TContext extends CartesianSeriesNodeDataContext = CartesianSeriesNodeDataContext, > extends CartesianSeries { - @Validate(DIRECTION) - direction: Direction = 'vertical'; + abstract override properties: AbstractBarSeriesProperties; override getBandScalePadding() { return { inner: 0.2, outer: 0.1 }; } override shouldFlipXY(): boolean { - return this.direction === 'horizontal'; + return !this.isVertical(); + } + + protected isVertical(): boolean { + return this.properties.direction === 'vertical'; } protected getBarDirection() { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/areaSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/areaSeries.ts index a9f967c1eb..8dcbf6001d 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/areaSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/areaSeries.ts @@ -2,13 +2,7 @@ import type { ModuleContext } from '../../../module/moduleContext'; import { fromToMotion } from '../../../motion/fromToMotion'; import { pathMotion } from '../../../motion/pathMotion'; import { resetMotion } from '../../../motion/resetMotion'; -import type { - AgAreaSeriesLabelFormatterParams, - AgAreaSeriesOptionsKeys, - AgCartesianSeriesTooltipRendererParams, -} from '../../../options/agChartOptions'; import { ContinuousScale } from '../../../scale/continuousScale'; -import type { DropShadow } from '../../../scene/dropShadow'; import { Group } from '../../../scene/group'; import { PointerEvents } from '../../../scene/node'; import { Path2D } from '../../../scene/path2D'; @@ -19,7 +13,7 @@ import type { Text } from '../../../scene/shape/text'; import { extent } from '../../../util/array'; import { mergeDefaults } from '../../../util/object'; import { sanitizeHtml } from '../../../util/sanitize'; -import { COLOR_STRING, LINE_DASH, POSITIVE_NUMBER, RATIO, STRING, Validate } from '../../../util/validation'; +import { isDefined } from '../../../util/type-guards'; import { isContinuous, isNumber } from '../../../util/value'; import { LogAxis } from '../../axis/logAxis'; import { TimeAxis } from '../../axis/timeAxis'; @@ -28,14 +22,12 @@ import type { DataController } from '../../data/dataController'; import type { DatumPropertyDefinition } from '../../data/dataModel'; import { fixNumericExtent } from '../../data/dataModel'; import { animationValidation, diff, normaliseGroupTo } from '../../data/processors'; -import { Label } from '../../label'; import type { CategoryLegendDatum, ChartLegendType } from '../../legendDatum'; import type { Marker } from '../../marker/marker'; import { getMarker } from '../../marker/util'; import { SeriesNodePickMode, groupAccumulativeValueProperty, keyProperty, valueProperty } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation } from '../seriesLabelUtil'; -import { SeriesMarker } from '../seriesMarker'; -import { SeriesTooltip } from '../seriesTooltip'; +import { AreaSeriesProperties } from './areaSeriesProperties'; import { type AreaPathPoint, type AreaSeriesNodeDataContext, @@ -65,29 +57,7 @@ export class AreaSeries extends CartesianSeries< static className = 'AreaSeries'; static type = 'area' as const; - tooltip = new SeriesTooltip(); - - readonly marker = new SeriesMarker(); - - readonly label = new Label(); - - @Validate(COLOR_STRING) - fill: string = '#c16068'; - - @Validate(COLOR_STRING) - stroke: string = '#874349'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; + override properties = new AreaSeriesProperties(); constructor(moduleCtx: ModuleContext) { super({ @@ -105,31 +75,13 @@ export class AreaSeries extends CartesianSeries< }); } - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string; - - @Validate(STRING, { optional: true }) - yName?: string; - - @Validate(POSITIVE_NUMBER, { optional: true }) - normalizedTo?: number; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 2; - - shadow?: DropShadow = undefined; - override async processData(dataController: DataController) { - const { xKey, yKey, normalizedTo, data, visible, seriesGrouping: { groupIndex = this.id } = {} } = this; - - if (xKey == null || yKey == null || data == null) return; + if (this.data == null || !this.properties.isValid()) { + return; + } + const { data, visible, seriesGrouping: { groupIndex = this.id } = {} } = this; + const { xKey, yKey, normalizedTo } = this.properties; const animationEnabled = !this.ctx.animationManager.isSkipped(); const { isContinuousX, isContinuousY } = this.isContinuous(); const ids = [ @@ -141,10 +93,9 @@ export class AreaSeries extends CartesianSeries< ]; const extraProps = []; - const normaliseTo = normalizedTo && isFinite(normalizedTo) ? normalizedTo : undefined; - if (normaliseTo) { - extraProps.push(normaliseGroupTo(this, [ids[0], ids[1], ids[4]], normaliseTo, 'range')); - extraProps.push(normaliseGroupTo(this, [ids[2], ids[3]], normaliseTo, 'range')); + if (isDefined(normalizedTo)) { + extraProps.push(normaliseGroupTo(this, [ids[0], ids[1], ids[4]], normalizedTo, 'range')); + extraProps.push(normaliseGroupTo(this, [ids[2], ids[3]], normalizedTo, 'range')); } // If two or more datums share an x-value, i.e. lined up vertically, they will have the same datum id. @@ -231,11 +182,11 @@ export class AreaSeries extends CartesianSeries< const xAxis = axes[ChartAxisDirection.X]; const yAxis = axes[ChartAxisDirection.Y]; - if (!xAxis || !yAxis || !data || !dataModel) { + if (!xAxis || !yAxis || !data || !dataModel || !this.properties.isValid()) { return []; } - const { yKey = '', xKey = '', marker, label, fill: seriesFill, stroke: seriesStroke } = this; + const { yKey, xKey, marker, label, fill: seriesFill, stroke: seriesStroke } = this.properties; const { scale: xScale } = xAxis; const { scale: yScale } = yAxis; @@ -275,19 +226,15 @@ export class AreaSeries extends CartesianSeries< // if not normalized, the invalid data points will be processed as `undefined` in processData() // if normalized, the invalid data points will be processed as 0 rather than `undefined` // check if unprocessed datum is valid as we only want to show markers for valid points - const normalized = this.normalizedTo && isFinite(this.normalizedTo); - const normalizedAndValid = normalized && continuousY && isContinuous(rawYDatum); - - const valid = (!normalized && !isNaN(rawYDatum)) || normalizedAndValid; - - if (valid) { + if (isDefined(this.properties.normalizedTo) ? continuousY && isContinuous(rawYDatum) : !isNaN(rawYDatum)) { currY = yEnd; } - const x = xScale.convert(xDatum) + xOffset; - const y = yScale.convert(currY); - - return { x, y, size: marker.size }; + return { + x: xScale.convert(xDatum) + xOffset, + y: yScale.convert(currY), + size: marker.size, + }; }; const itemId = yKey; @@ -347,7 +294,7 @@ export class AreaSeries extends CartesianSeries< point, fill: marker.fill ?? seriesFill, stroke: marker.stroke ?? seriesStroke, - strokeWidth: marker.strokeWidth ?? this.getStrokeWidth(this.strokeWidth), + strokeWidth: marker.strokeWidth ?? this.getStrokeWidth(this.properties.strokeWidth), }); } @@ -360,8 +307,8 @@ export class AreaSeries extends CartesianSeries< datum: seriesDatum, xKey, yKey, - xName: this.xName, - yName: this.yName, + xName: this.properties.xName, + yName: this.properties.yName, }, (value) => (isNumber(value) ? value.toFixed(2) : String(value)) ); @@ -430,11 +377,11 @@ export class AreaSeries extends CartesianSeries< } protected override isPathOrSelectionDirty(): boolean { - return this.marker.isDirty(); + return this.properties.marker.isDirty(); } protected override markerFactory() { - const { shape } = this.marker; + const { shape } = this.properties.marker; const MarkerShape = getMarker(shape); return new MarkerShape(); } @@ -449,17 +396,17 @@ export class AreaSeries extends CartesianSeries< const [fill, stroke] = opts.paths; const { seriesRectHeight: height, seriesRectWidth: width } = this.nodeDataDependencies; - const strokeWidth = this.getStrokeWidth(this.strokeWidth); + const strokeWidth = this.getStrokeWidth(this.properties.strokeWidth); stroke.setProperties({ tag: AreaSeriesTag.Stroke, fill: undefined, lineJoin: (stroke.lineCap = 'round'), pointerEvents: PointerEvents.None, - stroke: this.stroke, + stroke: this.properties.stroke, strokeWidth, - strokeOpacity: this.strokeOpacity, - lineDash: this.lineDash, - lineDashOffset: this.lineDashOffset, + strokeOpacity: this.properties.strokeOpacity, + lineDash: this.properties.lineDash, + lineDashOffset: this.properties.lineDashOffset, opacity, visible, }); @@ -468,12 +415,12 @@ export class AreaSeries extends CartesianSeries< stroke: undefined, lineJoin: 'round', pointerEvents: PointerEvents.None, - fill: this.fill, - fillOpacity: this.fillOpacity, - lineDash: this.lineDash, - lineDashOffset: this.lineDashOffset, - strokeOpacity: this.strokeOpacity, - fillShadow: this.shadow, + fill: this.properties.fill, + fillOpacity: this.properties.fillOpacity, + lineDash: this.properties.lineDash, + lineDashOffset: this.properties.lineDashOffset, + strokeOpacity: this.properties.strokeOpacity, + fillShadow: this.properties.shadow, opacity, visible: visible || animationEnabled, strokeWidth, @@ -539,17 +486,13 @@ export class AreaSeries extends CartesianSeries< markerSelection: Selection; }) { const { nodeData, markerSelection } = opts; - const { - marker: { enabled }, - } = this; - const data = enabled && nodeData ? nodeData : []; - if (this.marker.isDirty()) { + if (this.properties.marker.isDirty()) { markerSelection.clear(); markerSelection.cleanup(); } - return markerSelection.update(data); + return markerSelection.update(this.properties.marker.enabled ? nodeData : []); } protected override async updateMarkerNodes(opts: { @@ -557,8 +500,9 @@ export class AreaSeries extends CartesianSeries< isHighlight: boolean; }) { const { markerSelection, isHighlight: highlighted } = opts; - const { xKey = '', yKey = '', marker, fill, stroke, strokeWidth, fillOpacity, strokeOpacity } = this; - const baseStyle = mergeDefaults(highlighted && this.highlightStyle.item, marker.getStyle(), { + const { xKey, yKey, marker, fill, stroke, strokeWidth, fillOpacity, strokeOpacity, highlightStyle } = + this.properties; + const baseStyle = mergeDefaults(highlighted && highlightStyle.item, marker.getStyle(), { fill, stroke, strokeWidth, @@ -571,7 +515,7 @@ export class AreaSeries extends CartesianSeries< }); if (!highlighted) { - this.marker.markClean(); + this.properties.marker.markClean(); } } @@ -588,7 +532,7 @@ export class AreaSeries extends CartesianSeries< protected async updateLabelNodes(opts: { labelSelection: Selection }) { const { labelSelection } = opts; - const { enabled: labelEnabled, fontStyle, fontWeight, fontSize, fontFamily, color } = this.label; + const { enabled: labelEnabled, fontStyle, fontWeight, fontSize, fontFamily, color } = this.properties.label; labelSelection.each((text, datum) => { const { x, y, label } = datum; @@ -611,13 +555,14 @@ export class AreaSeries extends CartesianSeries< } getTooltipHtml(nodeDatum: MarkerSelectionDatum): string { - const { xKey, id: seriesId, axes, xName, yName, tooltip, marker, dataModel } = this; + const { id: seriesId, axes, dataModel } = this; + const { xKey, xName, yName, tooltip, marker } = this.properties; const { yKey, xValue, yValue, datum } = nodeDatum; const xAxis = axes[ChartAxisDirection.X]; const yAxis = axes[ChartAxisDirection.Y]; - if (!(xKey && yKey) || !(xAxis && yAxis && isNumber(yValue)) || !dataModel) { + if (!this.properties.isValid() || !(xAxis && yAxis && isNumber(yValue)) || !dataModel) { return ''; } @@ -626,9 +571,9 @@ export class AreaSeries extends CartesianSeries< const title = sanitizeHtml(yName); const content = sanitizeHtml(xString + ': ' + yString); - const baseStyle = mergeDefaults({ fill: this.fill }, marker.getStyle(), { - stroke: this.stroke, - strokeWidth: this.strokeWidth, + const baseStyle = mergeDefaults({ fill: this.properties.fill }, marker.getStyle(), { + stroke: this.properties.stroke, + strokeWidth: this.properties.strokeWidth, }); const { fill: color } = this.getMarkerStyle( marker, @@ -652,18 +597,18 @@ export class AreaSeries extends CartesianSeries< } getLegendData(legendType: ChartLegendType): CategoryLegendDatum[] { - const { data, id, xKey, yKey, yName, marker, fill, stroke, fillOpacity, strokeOpacity, visible } = this; - - if (!data?.length || !xKey || !yKey || legendType !== 'category') { + if (!this.data?.length || !this.properties.isValid() || legendType !== 'category') { return []; } + const { yKey, yName, fill, stroke, fillOpacity, strokeOpacity, marker, visible } = this.properties; + return [ { legendType, - id, + id: this.id, itemId: yKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: yName ?? yKey, @@ -732,7 +677,7 @@ export class AreaSeries extends CartesianSeries< } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } protected nodeFactory() { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/areaSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/cartesian/areaSeriesProperties.ts new file mode 100644 index 0000000000..18e234f118 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/cartesian/areaSeriesProperties.ts @@ -0,0 +1,63 @@ +import type { + AgAreaSeriesLabelFormatterParams, + AgAreaSeriesOptionsKeys, + AgCartesianSeriesTooltipRendererParams, + AgSeriesAreaOptions, +} from '../../../options/agChartOptions'; +import { DropShadow } from '../../../scene/dropShadow'; +import { COLOR_STRING, LINE_DASH, OBJECT, POSITIVE_NUMBER, RATIO, STRING, Validate } from '../../../util/validation'; +import { Label } from '../../label'; +import { SeriesMarker } from '../seriesMarker'; +import { SeriesTooltip } from '../seriesTooltip'; +import type { MarkerSelectionDatum } from './areaUtil'; +import { CartesianSeriesProperties } from './cartesianSeries'; + +export class AreaSeriesProperties extends CartesianSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string = undefined; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + normalizedTo?: number; + + @Validate(COLOR_STRING) + fill: string = '#c16068'; + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(COLOR_STRING) + stroke: string = '#874349'; + + @Validate(POSITIVE_NUMBER) + strokeWidth = 2; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(OBJECT) + readonly shadow = new DropShadow(); + + @Validate(OBJECT) + readonly marker = new SeriesMarker(); + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-community/src/chart/series/cartesian/barSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/barSeries.ts index e634366d81..e8b6106ccc 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/barSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/barSeries.ts @@ -1,18 +1,9 @@ import type { ModuleContext } from '../../../module/moduleContext'; import { fromToMotion } from '../../../motion/fromToMotion'; -import type { - AgBarSeriesFormatterParams, - AgBarSeriesLabelFormatterParams, - AgBarSeriesLabelPlacement, - AgBarSeriesStyle, - AgBarSeriesTooltipRendererParams, - FontStyle, - FontWeight, -} from '../../../options/agChartOptions'; +import type { AgBarSeriesStyle, FontStyle, FontWeight } from '../../../options/agChartOptions'; import { BandScale } from '../../../scale/bandScale'; import { ContinuousScale } from '../../../scale/continuousScale'; import { BBox } from '../../../scene/bbox'; -import type { DropShadow } from '../../../scene/dropShadow'; import { PointerEvents } from '../../../scene/node'; import type { Point } from '../../../scene/point'; import type { Selection } from '../../../scene/selection'; @@ -20,17 +11,6 @@ import { Rect } from '../../../scene/shape/rect'; import type { Text } from '../../../scene/shape/text'; import { extent } from '../../../util/array'; import { sanitizeHtml } from '../../../util/sanitize'; -import { - COLOR_STRING, - FUNCTION, - LINE_DASH, - NUMBER, - PLACEMENT, - POSITIVE_NUMBER, - RATIO, - STRING, - Validate, -} from '../../../util/validation'; import { isNumber } from '../../../util/value'; import { CategoryAxis } from '../../axis/categoryAxis'; import { GroupedCategoryAxis } from '../../axis/groupedCategoryAxis'; @@ -39,13 +19,12 @@ import { ChartAxisDirection } from '../../chartAxisDirection'; import type { DataController } from '../../data/dataController'; import { fixNumericExtent } from '../../data/dataModel'; import { SMALLEST_KEY_INTERVAL, animationValidation, diff, normaliseGroupTo } from '../../data/processors'; -import { Label } from '../../label'; import type { CategoryLegendDatum, ChartLegendType } from '../../legendDatum'; import { SeriesNodePickMode, groupAccumulativeValueProperty, keyProperty, valueProperty } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation } from '../seriesLabelUtil'; -import { SeriesTooltip } from '../seriesTooltip'; import type { ErrorBoundSeriesNodeDatum } from '../seriesTypes'; import { AbstractBarSeries } from './abstractBarSeries'; +import { BarSeriesProperties } from './barSeriesProperties'; import type { RectConfig } from './barUtil'; import { checkCrisp, @@ -97,65 +76,11 @@ enum BarSeriesNodeTag { Label, } -class BarSeriesLabel extends Label { - @Validate(PLACEMENT) - placement: AgBarSeriesLabelPlacement = 'inside'; -} - export class BarSeries extends AbstractBarSeries { static className = 'BarSeries'; static type = 'bar' as const; - readonly label = new BarSeriesLabel(); - - tooltip = new SeriesTooltip(); - - @Validate(COLOR_STRING, { optional: true }) - fill: string = '#c16068'; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = '#874349'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgBarSeriesFormatterParams) => AgBarSeriesStyle = undefined; - - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(STRING, { optional: true }) - stackGroup?: string = undefined; - - @Validate(NUMBER, { optional: true }) - normalizedTo?: number; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; - - @Validate(POSITIVE_NUMBER) - cornerRadius: number = 0; - - shadow?: DropShadow = undefined; + override properties = new BarSeriesProperties(); constructor(moduleCtx: ModuleContext) { super({ @@ -189,9 +114,12 @@ export class BarSeries extends AbstractBarSeries { protected smallestDataInterval?: { x: number; y: number } = undefined; override async processData(dataController: DataController) { - const { xKey, yKey, normalizedTo, seriesGrouping: { groupIndex = this.id } = {}, data = [] } = this; + if (!this.properties.isValid() || !this.data) { + return; + } - if (xKey == null || yKey == null || data == null) return; + const { seriesGrouping: { groupIndex = this.id } = {}, data = [] } = this; + const { xKey, yKey, normalizedTo } = this.properties; const animationEnabled = !this.ctx.animationManager.isSkipped(); const normalizedToAbs = Math.abs(normalizedTo ?? NaN); @@ -287,7 +215,7 @@ export class BarSeries extends AbstractBarSeries { const xAxis = this.getCategoryAxis(); const yAxis = this.getValueAxis(); - if (!(dataModel && xAxis && yAxis)) { + if (!(dataModel && xAxis && yAxis && this.properties.isValid())) { return []; } @@ -296,17 +224,12 @@ export class BarSeries extends AbstractBarSeries { const { groupScale, - yKey = '', - xKey = '', - fill, - stroke, - strokeWidth, - cornerRadius, - label, processedData, - ctx: { seriesStateManager }, smallestDataInterval, + ctx: { seriesStateManager }, } = this; + const { xKey, yKey, xName, yName, fill, stroke, strokeWidth, cornerRadius, legendItemName, label } = + this.properties; const xBandWidth = ContinuousScale.is(xScale) ? xScale.calcBandwidth(smallestDataInterval?.x) @@ -399,15 +322,15 @@ export class BarSeries extends AbstractBarSeries { } = label; const labelText = this.getLabelText( - this.label, + this.properties.label, { datum: seriesDatum[0], value: yRawValue, xKey, yKey, - xName: this.xName, - yName: this.yName, - legendItemName: this.legendItemName, + xName, + yName, + legendItemName, }, (value) => (isNumber(value) ? value.toFixed(2) : '') ); @@ -485,28 +408,30 @@ export class BarSeries extends AbstractBarSeries { datumSelection: Selection; isHighlight: boolean; }) { - const { datumSelection, isHighlight } = opts; + if (!this.properties.isValid()) { + return; + } + const { - yKey = '', + yKey, + stackGroup, fill, - stroke, fillOpacity, + stroke, + strokeWidth, strokeOpacity, lineDash, lineDashOffset, - shadow, formatter, - id: seriesId, + shadow, highlightStyle: { item: itemHighlightStyle }, - ctx, - stackGroup, - } = this; + } = this.properties; const xAxis = this.axes[ChartAxisDirection.X]; const crisp = checkCrisp(xAxis?.visibleRange); const categoryAlongX = this.getCategoryDirection() === ChartAxisDirection.X; - datumSelection.each((rect, datum) => { + opts.datumSelection.each((rect, datum) => { const style: RectConfig = { fill, stroke, @@ -515,7 +440,7 @@ export class BarSeries extends AbstractBarSeries { lineDash, lineDashOffset, fillShadow: shadow, - strokeWidth: this.getStrokeWidth(this.strokeWidth), + strokeWidth: this.getStrokeWidth(strokeWidth), topLeftCornerRadius: datum.topLeftCornerRadius, topRightCornerRadius: datum.topRightCornerRadius, bottomRightCornerRadius: datum.bottomRightCornerRadius, @@ -526,14 +451,14 @@ export class BarSeries extends AbstractBarSeries { const config = getRectConfig({ datum, - isHighlighted: isHighlight, - style, + ctx: this.ctx, + seriesId: this.id, + isHighlighted: opts.isHighlight, highlightStyle: itemHighlightStyle, + yKey, + style, formatter, - seriesId, stackGroup, - ctx, - yKey, }); config.crisp = crisp; config.visible = visible; @@ -545,7 +470,7 @@ export class BarSeries extends AbstractBarSeries { labelData: BarNodeDatum[]; labelSelection: Selection; }) { - const data = this.label.enabled ? opts.labelData : []; + const data = this.isLabelEnabled() ? opts.labelData : []; return opts.labelSelection.update(data, (text) => { text.tag = BarSeriesNodeTag.Label; text.pointerEvents = PointerEvents.None; @@ -554,27 +479,26 @@ export class BarSeries extends AbstractBarSeries { protected async updateLabelNodes(opts: { labelSelection: Selection }) { opts.labelSelection.each((textNode, datum) => { - updateLabelNode(textNode, this.label, datum.label); + updateLabelNode(textNode, this.properties.label, datum.label); }); } getTooltipHtml(nodeDatum: BarNodeDatum): string { const { - xKey, - yKey, + id: seriesId, processedData, ctx: { callbackCache }, } = this; const xAxis = this.getCategoryAxis(); const yAxis = this.getValueAxis(); - const { xValue, yValue, datum } = nodeDatum; - if (!processedData || !xKey || !yKey || !xAxis || !yAxis) { + if (!processedData || !this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { xName, yName, fill, stroke, tooltip, formatter, id: seriesId, stackGroup } = this; - const strokeWidth = this.getStrokeWidth(this.strokeWidth); + const { xKey, yKey, xName, yName, fill, stroke, strokeWidth, tooltip, formatter, stackGroup } = this.properties; + const { xValue, yValue, datum } = nodeDatum; + const xString = xAxis.formatDatum(xValue); const yString = yAxis.formatDatum(yValue); const title = sanitizeHtml(yName); @@ -584,15 +508,15 @@ export class BarSeries extends AbstractBarSeries { if (formatter) { format = callbackCache.call(formatter, { + seriesId, datum, - fill, - stroke, - strokeWidth, - highlighted: false, xKey, yKey, - seriesId, stackGroup, + fill, + stroke, + strokeWidth: this.getStrokeWidth(strokeWidth), + highlighted: false, }); } @@ -601,59 +525,46 @@ export class BarSeries extends AbstractBarSeries { return tooltip.toTooltipHtml( { title, content, backgroundColor: color }, { + seriesId, datum, xKey, - xName, yKey, + xName, yName, - color, - title, - seriesId, stackGroup, + title, + color, ...this.getModuleTooltipParams(), } ); } getLegendData(legendType: ChartLegendType): CategoryLegendDatum[] { - const { - id, - data, - xKey, - yKey, - yName, - legendItemName, - fill, - stroke, - strokeWidth, - fillOpacity, - strokeOpacity, - visible, - showInLegend, - } = this; + const { showInLegend } = this.properties; - if (legendType !== 'category' || !showInLegend || !data?.length || !xKey || !yKey) { + if (legendType !== 'category' || !this.data?.length || !this.properties.isValid() || !showInLegend) { return []; } + const { yKey, yName, fill, stroke, strokeWidth, fillOpacity, strokeOpacity, legendItemName, visible } = + this.properties; + return [ { legendType: 'category', - id, + id: this.id, itemId: yKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: legendItemName ?? yName ?? yKey }, - marker: { fill, stroke, fillOpacity, strokeOpacity, strokeWidth }, + marker: { fill, fillOpacity, stroke, strokeWidth, strokeOpacity }, legendItemName, }, ]; } override animateEmptyUpdateReady({ datumSelections, labelSelections, annotationSelections }: BarAnimationData) { - const fns = prepareBarAnimationFunctions( - collapsedStartingBarPosition(this.direction === 'vertical', this.axes) - ); + const fns = prepareBarAnimationFunctions(collapsedStartingBarPosition(this.isVertical(), this.axes)); fromToMotion(this.id, 'nodes', this.ctx.animationManager, datumSelections, fns); seriesLabelFadeInAnimation(this, 'labels', this.ctx.animationManager, labelSelections); @@ -666,9 +577,7 @@ export class BarSeries extends AbstractBarSeries { this.ctx.animationManager.stopByAnimationGroupId(this.id); const diff = this.processedData?.reduced?.diff; - const fns = prepareBarAnimationFunctions( - collapsedStartingBarPosition(this.direction === 'vertical', this.axes) - ); + const fns = prepareBarAnimationFunctions(collapsedStartingBarPosition(this.isVertical(), this.axes)); fromToMotion( this.id, @@ -685,6 +594,6 @@ export class BarSeries extends AbstractBarSeries { } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } } diff --git a/packages/ag-charts-community/src/chart/series/cartesian/barSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/cartesian/barSeriesProperties.ts new file mode 100644 index 0000000000..0b170c5874 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/cartesian/barSeriesProperties.ts @@ -0,0 +1,85 @@ +import type { + AgBarSeriesFormatterParams, + AgBarSeriesLabelFormatterParams, + AgBarSeriesLabelPlacement, + AgBarSeriesOptions, + AgBarSeriesStyle, + AgBarSeriesTooltipRendererParams, +} from '../../../options/series/cartesian/barOptions'; +import { DropShadow } from '../../../scene/dropShadow'; +import { + COLOR_STRING, + FUNCTION, + LINE_DASH, + NUMBER, + OBJECT, + PLACEMENT, + POSITIVE_NUMBER, + RATIO, + STRING, + Validate, +} from '../../../util/validation'; +import { Label } from '../../label'; +import { SeriesTooltip } from '../seriesTooltip'; +import { AbstractBarSeriesProperties } from './abstractBarSeries'; + +class BarSeriesLabel extends Label { + @Validate(PLACEMENT) + placement: AgBarSeriesLabelPlacement = 'inside'; +} + +export class BarSeriesProperties extends AbstractBarSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + stackGroup?: string; + + @Validate(NUMBER, { optional: true }) + normalizedTo?: number; + + @Validate(COLOR_STRING) + fill: string = '#c16068'; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING) + stroke: string = '#874349'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash?: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(POSITIVE_NUMBER) + cornerRadius: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgBarSeriesFormatterParams) => AgBarSeriesStyle; + + @Validate(OBJECT, { optional: true }) + readonly shadow = new DropShadow(); + + @Validate(OBJECT) + readonly label = new BarSeriesLabel(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-community/src/chart/series/cartesian/barUtil.ts b/packages/ag-charts-community/src/chart/series/cartesian/barUtil.ts index 4c0fa73bf0..a840ff1e7a 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/barUtil.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/barUtil.ts @@ -10,7 +10,7 @@ import { isNegative } from '../../../util/number'; import { mergeDefaults } from '../../../util/object'; import type { ChartAxis } from '../../chartAxis'; import { ChartAxisDirection } from '../../chartAxisDirection'; -import type { SeriesItemHighlightStyle } from '../series'; +import type { SeriesItemHighlightStyle } from '../seriesProperties'; import type { CartesianSeriesNodeDatum } from './cartesianSeries'; export type RectConfig = { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeries.ts index 35fb1580f5..a62d32cce6 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeries.ts @@ -1,13 +1,7 @@ import type { ModuleContext } from '../../../module/moduleContext'; -import type { - AgBubbleSeriesLabelFormatterParams, - AgBubbleSeriesOptionsKeys, - AgBubbleSeriesTooltipRendererParams, -} from '../../../options/agChartOptions'; import { ColorScale } from '../../../scale/colorScale'; import { LinearScale } from '../../../scale/linearScale'; import { HdpiCanvas } from '../../../scene/canvas/hdpiCanvas'; -import { RedrawType, SceneChangeDetection } from '../../../scene/changeDetectable'; import { Group } from '../../../scene/group'; import type { Selection } from '../../../scene/selection'; import type { Text } from '../../../scene/shape/text'; @@ -15,25 +9,22 @@ import { extent } from '../../../util/array'; import type { MeasuredLabel, PointLabelDatum } from '../../../util/labelPlacement'; import { mergeDefaults } from '../../../util/object'; import { sanitizeHtml } from '../../../util/sanitize'; -import { COLOR_STRING_ARRAY, NUMBER_ARRAY, POSITIVE_NUMBER, STRING, Validate } from '../../../util/validation'; import { ChartAxisDirection } from '../../chartAxisDirection'; import type { DataController } from '../../data/dataController'; import { fixNumericExtent } from '../../data/dataModel'; import { createDatumId } from '../../data/processors'; -import { Label } from '../../label'; import type { CategoryLegendDatum } from '../../legendDatum'; import type { Marker } from '../../marker/marker'; import { getMarker } from '../../marker/util'; import type { SeriesNodeEventTypes } from '../series'; import { SeriesNodePickMode, keyProperty, valueProperty } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation } from '../seriesLabelUtil'; -import { SeriesMarker } from '../seriesMarker'; -import { SeriesTooltip } from '../seriesTooltip'; +import { BubbleSeriesProperties } from './bubbleSeriesProperties'; import type { CartesianAnimationData, CartesianSeriesNodeDatum } from './cartesianSeries'; import { CartesianSeries, CartesianSeriesNodeClickEvent } from './cartesianSeries'; import { markerScaleInAnimation, resetMarkerFn } from './markerUtil'; -interface BubbleNodeDatum extends Required { +export interface BubbleNodeDatum extends Required { readonly sizeValue: any; readonly label: MeasuredLabel; readonly fill: string | undefined; @@ -48,80 +39,21 @@ class BubbleSeriesNodeClickEvent< constructor(type: TEvent, nativeEvent: MouseEvent, datum: BubbleNodeDatum, series: BubbleSeries) { super(type, nativeEvent, datum, series); - this.sizeKey = series.sizeKey; + this.sizeKey = series.properties.sizeKey; } } -class BubbleSeriesMarker extends SeriesMarker { - /** - * The series `sizeKey` values along with the `size` and `maxSize` configs will be used to - * determine the size of the marker. All values will be mapped to a marker size within the - * `[size, maxSize]` range, where the largest values will correspond to the `maxSize` and the - * lowest to the `size`. - */ - @Validate(POSITIVE_NUMBER) - @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - maxSize = 30; - - @Validate(NUMBER_ARRAY, { optional: true }) - @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - domain?: [number, number] = undefined; -} - export class BubbleSeries extends CartesianSeries { static className = 'BubbleSeries'; static type = 'bubble' as const; protected override readonly NodeClickEvent = BubbleSeriesNodeClickEvent; - private sizeScale = new LinearScale(); - - readonly marker = new BubbleSeriesMarker(); - - readonly label = new Label(); - - @Validate(STRING, { optional: true }) - title?: string = undefined; - - @Validate(STRING, { optional: true }) - labelKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(STRING, { optional: true }) - sizeName?: string = 'Size'; - - @Validate(STRING, { optional: true }) - labelName?: string = 'Label'; + override properties = new BubbleSeriesProperties(); - @Validate(STRING, { optional: true }) - xKey?: string = undefined; + private readonly sizeScale = new LinearScale(); - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - sizeKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorName?: string = 'Color'; - - @Validate(NUMBER_ARRAY, { optional: true }) - colorDomain?: number[]; - - @Validate(COLOR_STRING_ARRAY) - colorRange: string[] = ['#ffff00', '#00ff00', '#0000ff']; - - colorScale = new ColorScale(); - - readonly tooltip = new SeriesTooltip(); + private readonly colorScale = new ColorScale(); constructor(moduleCtx: ModuleContext) { super({ @@ -142,13 +74,13 @@ export class BubbleSeries extends CartesianSeries { } override async processData(dataController: DataController) { - const { xKey, yKey, sizeKey, labelKey, colorScale, colorDomain, colorRange, colorKey, marker, data } = this; - - if (xKey == null || yKey == null || sizeKey == null || data == null) return; + if (!this.properties.isValid() || this.data == null) { + return; + } const { isContinuousX, isContinuousY } = this.isContinuous(); - - const { dataModel, processedData } = await this.requestDataModel(dataController, data, { + const { xKey, yKey, sizeKey, labelKey, colorDomain, colorRange, colorKey, marker } = this.properties; + const { dataModel, processedData } = await this.requestDataModel(dataController, this.data, { props: [ keyProperty(this, xKey, isContinuousX, { id: 'xKey-raw' }), keyProperty(this, yKey, isContinuousY, { id: 'yKey-raw' }), @@ -168,9 +100,9 @@ export class BubbleSeries extends CartesianSeries { if (colorKey) { const colorKeyIdx = dataModel.resolveProcessedDataIndexById(this, `colorValue`).index; - colorScale.domain = colorDomain ?? processedData.domain.values[colorKeyIdx] ?? []; - colorScale.range = colorRange; - colorScale.update(); + this.colorScale.domain = colorDomain ?? processedData.domain.values[colorKeyIdx] ?? []; + this.colorScale.range = colorRange; + this.colorScale.update(); } this.animationState.transition('updateData'); @@ -191,24 +123,16 @@ export class BubbleSeries extends CartesianSeries { } async createNodeData() { - const { - visible, - axes, - yKey = '', - xKey = '', - label, - labelKey, - dataModel, - processedData, - colorScale, - sizeKey = '', - colorKey, - } = this; + const { axes, dataModel, processedData, colorScale, sizeScale } = this; + const { xKey, yKey, sizeKey, labelKey, xName, yName, sizeName, labelName, label, colorKey, marker, visible } = + this.properties; const xAxis = axes[ChartAxisDirection.X]; const yAxis = axes[ChartAxisDirection.Y]; - if (!(dataModel && processedData && visible && xAxis && yAxis)) return []; + if (!(dataModel && processedData && visible && xAxis && yAxis)) { + return []; + } const xDataIdx = dataModel.resolveProcessedDataIndexById(this, `xValue`).index; const yDataIdx = dataModel.resolveProcessedDataIndexById(this, `yValue`).index; @@ -220,7 +144,6 @@ export class BubbleSeries extends CartesianSeries { const yScale = yAxis.scale; const xOffset = (xScale.bandwidth ?? 0) / 2; const yOffset = (yScale.bandwidth ?? 0) / 2; - const { sizeScale, marker } = this; const nodeData: BubbleNodeDatum[] = []; sizeScale.range = [marker.size, marker.maxSize]; @@ -239,10 +162,10 @@ export class BubbleSeries extends CartesianSeries { yKey, sizeKey, labelKey, - xName: this.xName, - yName: this.yName, - sizeName: this.sizeName, - labelName: this.labelName, + xName, + yName, + sizeName, + labelName, }); const size = HdpiCanvas.getTextSize(String(labelText), font); @@ -267,7 +190,7 @@ export class BubbleSeries extends CartesianSeries { return [ { - itemId: this.yKey ?? this.id, + itemId: yKey, nodeData, labelData: nodeData, scales: super.calculateScaling(), @@ -277,7 +200,7 @@ export class BubbleSeries extends CartesianSeries { } protected override isPathOrSelectionDirty(): boolean { - return this.marker.isDirty(); + return this.properties.marker.isDirty(); } override getLabelData(): PointLabelDatum[] { @@ -285,7 +208,7 @@ export class BubbleSeries extends CartesianSeries { } protected override markerFactory() { - const { shape } = this.marker; + const { shape } = this.properties.marker; const MarkerShape = getMarker(shape); return new MarkerShape(); } @@ -296,12 +219,12 @@ export class BubbleSeries extends CartesianSeries { }) { const { nodeData, markerSelection } = opts; - if (this.marker.isDirty()) { + if (this.properties.marker.isDirty()) { markerSelection.clear(); markerSelection.cleanup(); } - const data = this.marker.enabled ? nodeData : []; + const data = this.properties.marker.enabled ? nodeData : []; return markerSelection.update(data, undefined, (datum) => this.getDatumId(datum)); } @@ -310,8 +233,8 @@ export class BubbleSeries extends CartesianSeries { isHighlight: boolean; }) { const { markerSelection, isHighlight: highlighted } = opts; - const { xKey = '', yKey = '', sizeKey = '', labelKey, marker } = this; - const baseStyle = mergeDefaults(highlighted && this.highlightStyle.item, marker.getStyle()); + const { xKey, yKey, sizeKey, labelKey, marker } = this.properties; + const baseStyle = mergeDefaults(highlighted && this.properties.highlightStyle.item, marker.getStyle()); this.sizeScale.range = [marker.size, marker.maxSize]; @@ -320,7 +243,7 @@ export class BubbleSeries extends CartesianSeries { }); if (!highlighted) { - this.marker.markClean(); + this.properties.marker.markClean(); } } @@ -328,31 +251,23 @@ export class BubbleSeries extends CartesianSeries { labelData: BubbleNodeDatum[]; labelSelection: Selection; }) { - const { labelSelection } = opts; - const { - label: { enabled }, - } = this; - - const placedLabels = enabled ? this.chart?.placeLabels().get(this) ?? [] : []; - - const placedNodeDatum = placedLabels.map( - (v): BubbleNodeDatum => ({ + const placedLabels = this.properties.label.enabled ? this.chart?.placeLabels().get(this) ?? [] : []; + return opts.labelSelection.update( + placedLabels.map((v) => ({ ...(v.datum as BubbleNodeDatum), point: { x: v.x, y: v.y, size: v.datum.point.size, }, - }) + })) ); - return labelSelection.update(placedNodeDatum); } protected async updateLabelNodes(opts: { labelSelection: Selection }) { - const { labelSelection } = opts; - const { label } = this; + const { label } = this.properties; - labelSelection.each((text, datum) => { + opts.labelSelection.each((text, datum) => { text.text = datum.label.text; text.fill = label.color; text.x = datum.point?.x ?? 0; @@ -367,16 +282,15 @@ export class BubbleSeries extends CartesianSeries { } getTooltipHtml(nodeDatum: BubbleNodeDatum): string { - const { xKey, yKey, sizeKey, axes } = this; - - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - if (!xKey || !yKey || !xAxis || !yAxis || !sizeKey) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { marker, tooltip, xName, yName, sizeName, labelKey, labelName, id: seriesId } = this; + const { xKey, yKey, sizeKey, labelKey, xName, yName, sizeName, labelName, marker, tooltip } = this.properties; + const title = this.properties.title ?? yName; const baseStyle = mergeDefaults( { fill: nodeDatum.fill, strokeWidth: this.getStrokeWidth(marker.strokeWidth) }, @@ -389,7 +303,6 @@ export class BubbleSeries extends CartesianSeries { baseStyle ); - const title = this.title ?? yName; const { datum, xValue, @@ -426,25 +339,25 @@ export class BubbleSeries extends CartesianSeries { labelName, title, color, - seriesId, + seriesId: this.id, } ); } getLegendData(): CategoryLegendDatum[] { - const { id, data, xKey, yKey, sizeKey, yName, title, visible, marker } = this; - const { shape, fill, stroke, fillOpacity, strokeOpacity, strokeWidth } = marker; - - if (!(data?.length && xKey && yKey && sizeKey)) { + if (!this.data?.length || !this.properties.isValid()) { return []; } + const { yKey, yName, title, marker, visible } = this.properties; + const { shape, fill, stroke, fillOpacity, strokeOpacity, strokeWidth } = marker; + return [ { legendType: 'category', - id, + id: this.id, itemId: yKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: title ?? yName ?? yKey, @@ -471,7 +384,7 @@ export class BubbleSeries extends CartesianSeries { } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } protected nodeFactory() { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeriesProperties.ts new file mode 100644 index 0000000000..c85db189dd --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/cartesian/bubbleSeriesProperties.ts @@ -0,0 +1,79 @@ +import type { + AgBubbleSeriesLabelFormatterParams, + AgBubbleSeriesOptions, + AgBubbleSeriesOptionsKeys, + AgBubbleSeriesTooltipRendererParams, +} from '../../../options/agChartOptions'; +import { RedrawType, SceneChangeDetection } from '../../../scene/changeDetectable'; +import { COLOR_STRING_ARRAY, NUMBER_ARRAY, OBJECT, POSITIVE_NUMBER, STRING, Validate } from '../../../util/validation'; +import { Label } from '../../label'; +import { SeriesMarker } from '../seriesMarker'; +import { SeriesTooltip } from '../seriesTooltip'; +import type { BubbleNodeDatum } from './bubbleSeries'; +import { CartesianSeriesProperties } from './cartesianSeries'; + +class BubbleSeriesMarker extends SeriesMarker { + /** + * The series `sizeKey` values along with the `size` and `maxSize` configs will be used to + * determine the size of the marker. All values will be mapped to a marker size within the + * `[size, maxSize]` range, where the largest values will correspond to the `maxSize` and the + * lowest to the `size`. + */ + @Validate(POSITIVE_NUMBER) + @SceneChangeDetection({ redraw: RedrawType.MAJOR }) + maxSize = 30; + + @Validate(NUMBER_ARRAY, { optional: true }) + @SceneChangeDetection({ redraw: RedrawType.MAJOR }) + domain?: [number, number]; +} + +export class BubbleSeriesProperties extends CartesianSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING) + sizeKey!: string; + + @Validate(STRING, { optional: true }) + labelKey?: string; + + @Validate(STRING, { optional: true }) + colorKey?: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + sizeName?: string; + + @Validate(STRING, { optional: true }) + labelName?: string; + + @Validate(STRING, { optional: true }) + colorName?: string; + + @Validate(NUMBER_ARRAY, { optional: true }) + colorDomain?: number[]; + + @Validate(COLOR_STRING_ARRAY) + colorRange: string[] = ['#ffff00', '#00ff00', '#0000ff']; + + @Validate(STRING, { optional: true }) + title?: string; + + @Validate(OBJECT) + readonly marker = new BubbleSeriesMarker(); + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-community/src/chart/series/cartesian/cartesianSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/cartesianSeries.ts index 7642581fbc..8d0299f1bf 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/cartesianSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/cartesianSeries.ts @@ -23,6 +23,7 @@ import { DataModelSeries } from '../dataModelSeries'; import type { Series, SeriesNodeDataContext, SeriesNodeEventTypes, SeriesNodePickMatch } from '../series'; import { SeriesNodeClickEvent } from '../series'; import type { SeriesGroupZIndexSubOrderType } from '../seriesLayerManager'; +import { SeriesProperties } from '../seriesProperties'; import type { SeriesNodeDatum } from '../seriesTypes'; export interface CartesianSeriesNodeDatum extends SeriesNodeDatum { @@ -124,6 +125,11 @@ export interface CategoryScaling { range: number[]; } +export abstract class CartesianSeriesProperties extends SeriesProperties { + @Validate(STRING, { optional: true }) + legendItemName?: string; +} + export interface CartesianSeriesNodeDataContext< TDatum extends CartesianSeriesNodeDatum = CartesianSeriesNodeDatum, TLabel extends SeriesNodeDatum = TDatum, @@ -139,8 +145,7 @@ export abstract class CartesianSeries< TLabel extends SeriesNodeDatum = TDatum, TContext extends CartesianSeriesNodeDataContext = CartesianSeriesNodeDataContext, > extends DataModelSeries { - @Validate(STRING, { optional: true }) - legendItemName?: string = undefined; + abstract override properties: CartesianSeriesProperties; private _contextNodeData: TContext[] = []; get contextNodeData() { @@ -303,11 +308,7 @@ export abstract class CartesianSeries< subGroup.datumSelection = await this.updateDatumSelection({ nodeData, datumSelection, seriesIdx }); subGroup.labelSelection = await this.updateLabelSelection({ labelData, labelSelection, seriesIdx }); if (markerSelection) { - subGroup.markerSelection = await this.updateMarkerSelection({ - nodeData, - markerSelection, - seriesIdx, - }); + subGroup.markerSelection = await this.updateMarkerSelection({ nodeData, markerSelection, seriesIdx }); } } @@ -717,20 +718,22 @@ export abstract class CartesianSeries< } onLegendItemClick(event: LegendItemClickChartEvent) { - const { enabled, itemId, series, legendItemName } = event; + const { legendItemName } = this.properties; + const { enabled, itemId, series } = event; - const matchedLegendItemName = this.legendItemName != null && this.legendItemName === legendItemName; + const matchedLegendItemName = legendItemName != null && legendItemName === event.legendItemName; if (series.id === this.id || matchedLegendItemName) { this.toggleSeriesItem(itemId, enabled); } } onLegendItemDoubleClick(event: LegendItemDoubleClickChartEvent) { - const { enabled, itemId, series, numVisibleItems, legendItemName } = event; + const { enabled, itemId, series, numVisibleItems } = event; + const { legendItemName } = this.properties; const totalVisibleItems = Object.values(numVisibleItems).reduce((p, v) => p + v, 0); - const matchedLegendItemName = this.legendItemName != null && this.legendItemName === legendItemName; + const matchedLegendItemName = legendItemName != null && legendItemName === event.legendItemName; if (series.id === this.id || matchedLegendItemName) { // Double-clicked item should always become visible. this.toggleSeriesItem(itemId, true); @@ -764,7 +767,9 @@ export abstract class CartesianSeries< override getMinRect() { const [context] = this._contextNodeData; - if (!context || context.nodeData.length == 0) return; + if (!context?.nodeData.length) { + return; + } const width = context.nodeData .map(({ midPoint }) => midPoint?.x ?? 0) @@ -806,10 +811,11 @@ export abstract class CartesianSeries< items?: TLabel[]; highlightLabelSelection: Selection; }): Promise> { - const { items, highlightLabelSelection } = opts; - const labelData = items ?? []; - - return this.updateLabelSelection({ labelData, labelSelection: highlightLabelSelection, seriesIdx: -1 }); + return this.updateLabelSelection({ + labelData: opts.items ?? [], + labelSelection: opts.highlightLabelSelection, + seriesIdx: -1, + }); } protected async updateDatumSelection(opts: { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/histogramSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/histogramSeries.ts index bcf7ff26fb..9b4dff1d9f 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/histogramSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/histogramSeries.ts @@ -1,43 +1,25 @@ import type { ModuleContext } from '../../../module/moduleContext'; import { fromToMotion } from '../../../motion/fromToMotion'; -import type { - AgHistogramSeriesLabelFormatterParams, - AgHistogramSeriesOptions, - AgHistogramSeriesTooltipRendererParams, - AgTooltipRendererResult, -} from '../../../options/agChartOptions'; +import type { AgTooltipRendererResult } from '../../../options/agChartOptions'; import type { FontStyle, FontWeight } from '../../../options/chart/types'; -import type { DropShadow } from '../../../scene/dropShadow'; import { PointerEvents } from '../../../scene/node'; import type { Selection } from '../../../scene/selection'; import { Rect } from '../../../scene/shape/rect'; import type { Text } from '../../../scene/shape/text'; import { sanitizeHtml, tickStep, ticks } from '../../../sparklines-util'; import { isReal } from '../../../util/number'; -import { - ARRAY, - BOOLEAN, - COLOR_STRING, - LINE_DASH, - POSITIVE_NUMBER, - RATIO, - STRING, - UNION, - Validate, -} from '../../../util/validation'; import { ChartAxisDirection } from '../../chartAxisDirection'; import { area, groupAverage, groupCount, groupSum } from '../../data/aggregateFunctions'; import type { DataController } from '../../data/dataController'; import type { PropertyDefinition } from '../../data/dataModel'; import { type AggregatePropertyDefinition, type GroupByFn, fixNumericExtent } from '../../data/dataModel'; import { SORT_DOMAIN_GROUPS, createDatumId, diff } from '../../data/processors'; -import { Label } from '../../label'; import type { CategoryLegendDatum, ChartLegendType } from '../../legendDatum'; import { Series, SeriesNodePickMode, keyProperty, valueProperty } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation } from '../seriesLabelUtil'; -import { SeriesTooltip } from '../seriesTooltip'; import { collapsedStartingBarPosition, prepareBarAnimationFunctions, resetBarSelectionsFn } from './barUtil'; import { type CartesianAnimationData, CartesianSeries, type CartesianSeriesNodeDatum } from './cartesianSeries'; +import { HistogramSeriesProperties } from './histogramSeriesProperties'; enum HistogramSeriesNodeTag { Bin, @@ -46,7 +28,7 @@ enum HistogramSeriesNodeTag { const defaultBinCount = 10; -interface HistogramNodeDatum extends CartesianSeriesNodeDatum { +export interface HistogramNodeDatum extends CartesianSeriesNodeDatum { readonly x: number; readonly y: number; readonly width: number; @@ -69,35 +51,13 @@ interface HistogramNodeDatum extends CartesianSeriesNodeDatum { }; } -type HistogramAggregation = NonNullable; - type HistogramAnimationData = CartesianAnimationData; export class HistogramSeries extends CartesianSeries { static className = 'HistogramSeries'; static type = 'histogram' as const; - readonly label = new Label(); - - tooltip = new SeriesTooltip>(); - - @Validate(COLOR_STRING, { optional: true }) - fill?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; + override properties = new HistogramSeriesProperties(); constructor(moduleCtx: ModuleContext) { super({ @@ -111,50 +71,21 @@ export class HistogramSeries extends CartesianSeries { }); } - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(BOOLEAN) - areaPlot: boolean = false; - - @Validate(ARRAY, { optional: true }) - bins?: [number, number][]; - - @Validate(UNION(['count', 'sum', 'mean'], 'a histogram aggregation')) - aggregation: HistogramAggregation = 'sum'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - binCount?: number = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; - - shadow?: DropShadow = undefined; calculatedBins: [number, number][] = []; // During processData phase, used to unify different ways of the user specifying // the bins. Returns bins in format[[min1, max1], [min2, max2], ... ]. private deriveBins(xDomain: [number, number]): [number, number][] { - if (this.binCount === undefined) { - const binStarts = ticks(xDomain[0], xDomain[1], this.binCount ?? defaultBinCount); - const binSize = tickStep(xDomain[0], xDomain[1], this.binCount ?? defaultBinCount); - const firstBinEnd = binStarts[0]; + if (this.properties.binCount) { + return this.calculateNiceBins(xDomain, this.properties.binCount); + } - const expandStartToBin: (n: number) => [number, number] = (n) => [n, n + binSize]; + const binStarts = ticks(xDomain[0], xDomain[1], defaultBinCount); + const binSize = tickStep(xDomain[0], xDomain[1], defaultBinCount); + const [firstBinEnd] = binStarts; - return [[firstBinEnd - binSize, firstBinEnd], ...binStarts.map(expandStartToBin)]; - } else { - return this.calculateNiceBins(xDomain, this.binCount); - } + const expandStartToBin = (n: number): [number, number] => [n, n + binSize]; + return [[firstBinEnd - binSize, firstBinEnd], ...binStarts.map(expandStartToBin)]; } private calculateNiceBins(domain: number[], binCount: number): [number, number][] { @@ -209,7 +140,7 @@ export class HistogramSeries extends CartesianSeries { } override async processData(dataController: DataController) { - const { xKey, yKey, data, areaPlot, aggregation } = this; + const { xKey, yKey, areaPlot, aggregation } = this.properties; const props: PropertyDefinition[] = [keyProperty(this, xKey, true), SORT_DOMAIN_GROUPS]; if (yKey) { @@ -243,7 +174,7 @@ export class HistogramSeries extends CartesianSeries { return () => []; } - const bins = this.bins ?? this.deriveBins(xExtent); + const bins = this.properties.bins ?? this.deriveBins(xExtent); const binCount = bins.length; this.calculatedBins = [...bins]; @@ -269,7 +200,7 @@ export class HistogramSeries extends CartesianSeries { props.push(diff(this.processedData, false)); } - await this.requestDataModel(dataController, data ?? [], { + await this.requestDataModel(dataController, this.data ?? [], { props, dataVisible: this.visible, groupByFn, @@ -295,6 +226,7 @@ export class HistogramSeries extends CartesianSeries { async createNodeData() { const { + id: seriesId, axes, processedData, ctx: { callbackCache }, @@ -309,21 +241,18 @@ export class HistogramSeries extends CartesianSeries { const { scale: xScale } = xAxis; const { scale: yScale } = yAxis; - const { fill, stroke, strokeWidth, id: seriesId, yKey = '', xKey = '' } = this; + const { xKey, yKey, xName, yName, fill, stroke, strokeWidth } = this.properties; + const { + formatter: labelFormatter = (params) => String(params.value), + fontStyle: labelFontStyle, + fontWeight: labelFontWeight, + fontSize: labelFontSize, + fontFamily: labelFontFamily, + color: labelColor, + } = this.properties.label; const nodeData: HistogramNodeDatum[] = []; - const { - label: { - formatter: labelFormatter = (params) => String(params.value), - fontStyle: labelFontStyle, - fontWeight: labelFontWeight, - fontSize: labelFontSize, - fontFamily: labelFontFamily, - color: labelColor, - }, - } = this; - processedData.data.forEach((group) => { const { aggValues: [[negativeAgg, positiveAgg]] = [[0, 0]], @@ -356,8 +285,8 @@ export class HistogramSeries extends CartesianSeries { seriesId, xKey, yKey, - xName: this.xName, - yName: this.yName, + xName, + yName, }) ?? String(total), fontStyle: labelFontStyle, fontWeight: labelFontWeight, @@ -399,7 +328,7 @@ export class HistogramSeries extends CartesianSeries { return [ { - itemId: this.yKey ?? this.id, + itemId: this.properties.yKey ?? this.id, nodeData, labelData: nodeData, scales: super.calculateScaling(), @@ -433,10 +362,12 @@ export class HistogramSeries extends CartesianSeries { datumSelection: Selection; isHighlight: boolean; }) { - const { datumSelection, isHighlight: isDatumHighlighted } = opts; + const { isHighlight: isDatumHighlighted } = opts; const { fillOpacity: seriesFillOpacity, strokeOpacity, + lineDash, + lineDashOffset, shadow, highlightStyle: { item: { @@ -446,9 +377,9 @@ export class HistogramSeries extends CartesianSeries { strokeWidth: highlightedDatumStrokeWidth, }, }, - } = this; + } = this.properties; - datumSelection.each((rect, datum, index) => { + opts.datumSelection.each((rect, datum, index) => { const strokeWidth = isDatumHighlighted && highlightedDatumStrokeWidth !== undefined ? highlightedDatumStrokeWidth @@ -460,8 +391,8 @@ export class HistogramSeries extends CartesianSeries { rect.fillOpacity = fillOpacity; rect.strokeOpacity = strokeOpacity; rect.strokeWidth = strokeWidth; - rect.lineDash = this.lineDash; - rect.lineDashOffset = this.lineDashOffset; + rect.lineDash = lineDash; + rect.lineDashOffset = lineDashOffset; rect.fillShadow = shadow; rect.zIndex = isDatumHighlighted ? Series.highlightedZIndex : index; rect.visible = datum.height > 0; // prevent stroke from rendering for zero height columns @@ -483,10 +414,9 @@ export class HistogramSeries extends CartesianSeries { } protected async updateLabelNodes(opts: { labelSelection: Selection }) { - const { labelSelection } = opts; - const labelEnabled = this.label.enabled; + const labelEnabled = this.isLabelEnabled(); - labelSelection.each((text, datum) => { + opts.labelSelection.each((text, datum) => { const label = datum.label; if (label && labelEnabled) { @@ -506,16 +436,14 @@ export class HistogramSeries extends CartesianSeries { } getTooltipHtml(nodeDatum: HistogramNodeDatum): string { - const { xKey, yKey = '', axes } = this; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; - - if (!xKey || !xAxis || !yAxis) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { xName, yName, fill: color, tooltip, aggregation, id: seriesId } = this; + const { xKey, yKey, xName, yName, fill: color, aggregation, tooltip } = this.properties; const { aggregatedValue, frequency, @@ -547,23 +475,23 @@ export class HistogramSeries extends CartesianSeries { yName, color, title, - seriesId, + seriesId: this.id, }); } getLegendData(legendType: ChartLegendType): CategoryLegendDatum[] { - const { id, data, xKey, yName, visible, fill, stroke, fillOpacity, strokeOpacity, strokeWidth } = this; - - if (!data || data.length === 0 || legendType !== 'category') { + if (!this.data?.length || legendType !== 'category') { return []; } + const { xKey, yName, fill, fillOpacity, stroke, strokeWidth, strokeOpacity, visible } = this.properties; + return [ { legendType: 'category', - id, + id: this.id, itemId: xKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: yName ?? xKey ?? 'Frequency', @@ -608,6 +536,6 @@ export class HistogramSeries extends CartesianSeries { } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } } diff --git a/packages/ag-charts-community/src/chart/series/cartesian/histogramSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/cartesian/histogramSeriesProperties.ts new file mode 100644 index 0000000000..49debc9512 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/cartesian/histogramSeriesProperties.ts @@ -0,0 +1,78 @@ +import type { + AgHistogramSeriesLabelFormatterParams, + AgHistogramSeriesOptions, + AgHistogramSeriesTooltipRendererParams, +} from '../../../options/agChartOptions'; +import { DropShadow } from '../../../scene/dropShadow'; +import { + ARRAY, + BOOLEAN, + COLOR_STRING, + LINE_DASH, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, + UNION, + Validate, +} from '../../../util/validation'; +import { Label } from '../../label'; +import { SeriesTooltip } from '../seriesTooltip'; +import { CartesianSeriesProperties } from './cartesianSeries'; +import type { HistogramNodeDatum } from './histogramSeries'; + +export class HistogramSeriesProperties extends CartesianSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING, { optional: true }) + yKey?: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(COLOR_STRING, { optional: true }) + fill?: string; + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(BOOLEAN) + areaPlot: boolean = false; + + @Validate(ARRAY, { optional: true }) + bins?: [number, number][]; + + @Validate(UNION(['count', 'sum', 'mean'], 'a histogram aggregation')) + aggregation: NonNullable = 'sum'; + + @Validate(POSITIVE_NUMBER, { optional: true }) + binCount?: number; + + @Validate(OBJECT) + readonly shadow = new DropShadow(); + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip>(); +} diff --git a/packages/ag-charts-community/src/chart/series/cartesian/lineSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/lineSeries.ts index 41e626bcaa..2d6a7e21d6 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/lineSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/lineSeries.ts @@ -2,13 +2,7 @@ import type { ModuleContext } from '../../../module/moduleContext'; import { fromToMotion } from '../../../motion/fromToMotion'; import { pathMotion } from '../../../motion/pathMotion'; import { resetMotion } from '../../../motion/resetMotion'; -import type { - AgLineSeriesLabelFormatterParams, - AgLineSeriesOptionsKeys, - AgLineSeriesTooltipRendererParams, - FontStyle, - FontWeight, -} from '../../../options/agChartOptions'; +import type { FontStyle, FontWeight } from '../../../options/agChartOptions'; import { Group } from '../../../scene/group'; import { PointerEvents } from '../../../scene/node'; import { Path2D } from '../../../scene/path2D'; @@ -18,21 +12,17 @@ import type { Text } from '../../../scene/shape/text'; import { extent } from '../../../util/array'; import { mergeDefaults } from '../../../util/object'; import { sanitizeHtml } from '../../../util/sanitize'; -import { COLOR_STRING, LINE_DASH, POSITIVE_NUMBER, RATIO, STRING, Validate } from '../../../util/validation'; import { isNumber } from '../../../util/value'; import { ChartAxisDirection } from '../../chartAxisDirection'; import type { DataController } from '../../data/dataController'; import type { DataModelOptions, UngroupedDataItem } from '../../data/dataModel'; import { fixNumericExtent } from '../../data/dataModel'; import { animationValidation, createDatumId, diff } from '../../data/processors'; -import { Label } from '../../label'; import type { CategoryLegendDatum, ChartLegendType } from '../../legendDatum'; import type { Marker } from '../../marker/marker'; import { getMarker } from '../../marker/util'; import { SeriesNodePickMode, keyProperty, valueProperty } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation } from '../seriesLabelUtil'; -import { SeriesMarker } from '../seriesMarker'; -import { SeriesTooltip } from '../seriesTooltip'; import type { ErrorBoundSeriesNodeDatum } from '../seriesTypes'; import type { CartesianAnimationData, @@ -40,11 +30,12 @@ import type { CartesianSeriesNodeDatum, } from './cartesianSeries'; import { CartesianSeries } from './cartesianSeries'; +import { LineSeriesProperties } from './lineSeriesProperties'; import { prepareLinePathAnimation } from './lineUtil'; import { markerSwipeScaleInAnimation, resetMarkerFn, resetMarkerPositionFn } from './markerUtil'; import { buildResetPathFn, pathSwipeInAnimation } from './pathUtil'; -interface LineNodeDatum extends CartesianSeriesNodeDatum, ErrorBoundSeriesNodeDatum { +export interface LineNodeDatum extends CartesianSeriesNodeDatum, ErrorBoundSeriesNodeDatum { readonly point: CartesianSeriesNodeDatum['point'] & { readonly moveTo: boolean; }; @@ -66,27 +57,7 @@ export class LineSeries extends CartesianSeries { static className = 'LineSeries'; static type = 'line' as const; - readonly label = new Label(); - readonly marker = new SeriesMarker(); - readonly tooltip = new SeriesTooltip(); - - @Validate(STRING, { optional: true }) - title?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = '#874349'; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 2; - - @Validate(RATIO) - strokeOpacity: number = 1; + override properties = new LineSeriesProperties(); constructor(moduleCtx: ModuleContext) { super({ @@ -106,31 +77,20 @@ export class LineSeries extends CartesianSeries { }); } - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - override async processData(dataController: DataController) { - const { xKey, yKey, data } = this; - - if (xKey == null || yKey == null || data == null) return; + if (!this.properties.isValid() || this.data == null) { + return; + } + const { xKey, yKey } = this.properties; const animationEnabled = !this.ctx.animationManager.isSkipped(); const { isContinuousX, isContinuousY } = this.isContinuous(); const props: DataModelOptions['props'] = []; - // If two or more datums share an x-value, i.e. lined up vertically, they will have the same datum id. + // If two or more datum share an x-value, i.e. lined up vertically, they will have the same datum id. // They must be identified this way when animated to ensure they can be tracked when their y-value - // is updated. If this is a static chart, we can instead not bother with identifying datums and + // is updated. If this is a static chart, we can instead not bother with identifying datum and // automatically garbage collect the marker selection. if (!isContinuousX) { props.push(keyProperty(this, xKey, isContinuousX, { id: 'xKey' })); @@ -147,7 +107,7 @@ export class LineSeries extends CartesianSeries { valueProperty(this, yKey, isContinuousY, { id: 'yValue', invalidValue: undefined }) ); - await this.requestDataModel(dataController, data, { props }); + await this.requestDataModel(dataController, this.data, { props }); this.animationState.transition('updateData'); } @@ -183,13 +143,13 @@ export class LineSeries extends CartesianSeries { return []; } - const { label, yKey = '', xKey = '' } = this; + const { xKey, yKey, xName, yName, marker, label } = this.properties; const xScale = xAxis.scale; const yScale = yAxis.scale; const xOffset = (xScale.bandwidth ?? 0) / 2; const yOffset = (yScale.bandwidth ?? 0) / 2; const nodeData: LineNodeDatum[] = []; - const size = this.marker.enabled ? this.marker.size : 0; + const size = marker.enabled ? marker.size : 0; const xIdx = dataModel.resolveProcessedDataIndexById(this, `xValue`).index; const yIdx = dataModel.resolveProcessedDataIndexById(this, `yValue`).index; @@ -218,14 +178,7 @@ export class LineSeries extends CartesianSeries { const labelText = this.getLabelText( label, - { - value: yDatum, - datum, - xKey, - yKey, - xName: this.xName, - yName: this.yName, - }, + { value: yDatum, datum, xKey, yKey, xName, yName }, (value) => (isNumber(value) ? value.toFixed(2) : String(value)) ); @@ -238,7 +191,7 @@ export class LineSeries extends CartesianSeries { midPoint: { x, y }, yValue: yDatum, xValue: xDatum, - capDefaults: { lengthRatioMultiplier: this.marker.getDiameter(), lengthMax: Infinity }, + capDefaults: { lengthRatioMultiplier: this.properties.marker.getDiameter(), lengthMax: Infinity }, label: labelText ? { text: labelText, @@ -268,11 +221,11 @@ export class LineSeries extends CartesianSeries { } protected override isPathOrSelectionDirty(): boolean { - return this.marker.isDirty(); + return this.properties.marker.isDirty(); } protected override markerFactory() { - const { shape } = this.marker; + const { shape } = this.properties.marker; const MarkerShape = getMarker(shape); return new MarkerShape(); } @@ -297,11 +250,11 @@ export class LineSeries extends CartesianSeries { lineJoin: 'round', pointerEvents: PointerEvents.None, opacity, - stroke: this.stroke, - strokeWidth: this.getStrokeWidth(this.strokeWidth), - strokeOpacity: this.strokeOpacity, - lineDash: this.lineDash, - lineDashOffset: this.lineDashOffset, + stroke: this.properties.stroke, + strokeWidth: this.getStrokeWidth(this.properties.strokeWidth), + strokeOpacity: this.properties.strokeOpacity, + lineDash: this.properties.lineDash, + lineDashOffset: this.properties.lineDashOffset, }); if (!animationEnabled) { @@ -324,10 +277,10 @@ export class LineSeries extends CartesianSeries { }) { let { nodeData } = opts; const { markerSelection } = opts; - const { shape, enabled } = this.marker; + const { shape, enabled } = this.properties.marker; nodeData = shape && enabled ? nodeData : []; - if (this.marker.isDirty()) { + if (this.properties.marker.isDirty()) { markerSelection.clear(); markerSelection.cleanup(); } @@ -340,8 +293,8 @@ export class LineSeries extends CartesianSeries { isHighlight: boolean; }) { const { markerSelection, isHighlight: highlighted } = opts; - const { xKey = '', yKey = '', marker, stroke, strokeWidth, strokeOpacity } = this; - const baseStyle = mergeDefaults(highlighted && this.highlightStyle.item, marker.getStyle(), { + const { xKey, yKey, stroke, strokeWidth, strokeOpacity, marker, highlightStyle } = this.properties; + const baseStyle = mergeDefaults(highlighted && highlightStyle.item, marker.getStyle(), { stroke, strokeWidth, strokeOpacity, @@ -353,7 +306,7 @@ export class LineSeries extends CartesianSeries { }); if (!highlighted) { - this.marker.markClean(); + marker.markClean(); } } @@ -361,22 +314,16 @@ export class LineSeries extends CartesianSeries { labelData: LineNodeDatum[]; labelSelection: Selection; }) { - let { labelData } = opts; - const { labelSelection } = opts; - const { enabled } = this.label; - labelData = enabled ? labelData : []; - - return labelSelection.update(labelData); + return opts.labelSelection.update(this.isLabelEnabled() ? opts.labelData : []); } protected async updateLabelNodes(opts: { labelSelection: Selection }) { - const { labelSelection } = opts; - const { enabled: labelEnabled, fontStyle, fontWeight, fontSize, fontFamily, color } = this.label; + const { enabled, fontStyle, fontWeight, fontSize, fontFamily, color } = this.properties.label; - labelSelection.each((text, datum) => { + opts.labelSelection.each((text, datum) => { const { point, label } = datum; - if (datum && label && labelEnabled) { + if (datum && label && enabled) { text.fontStyle = fontStyle; text.fontWeight = fontWeight; text.fontSize = fontSize; @@ -395,23 +342,21 @@ export class LineSeries extends CartesianSeries { } getTooltipHtml(nodeDatum: LineNodeDatum): string { - const { xKey, yKey, axes } = this; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; - - if (!xKey || !yKey || !xAxis || !yAxis) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { xName, yName, tooltip, marker, id: seriesId } = this; + const { xKey, yKey, xName, yName, strokeWidth, marker, tooltip } = this.properties; const { datum, xValue, yValue } = nodeDatum; const xString = xAxis.formatDatum(xValue); const yString = yAxis.formatDatum(yValue); - const title = sanitizeHtml(this.title ?? yName); + const title = sanitizeHtml(this.properties.title ?? yName); const content = sanitizeHtml(xString + ': ' + yString); - const baseStyle = mergeDefaults({ fill: marker.stroke }, marker.getStyle(), { strokeWidth: this.strokeWidth }); + const baseStyle = mergeDefaults({ fill: marker.stroke }, marker.getStyle(), { strokeWidth }); const { fill: color } = this.getMarkerStyle( marker, { datum: nodeDatum, xKey, yKey, highlighted: false }, @@ -428,25 +373,25 @@ export class LineSeries extends CartesianSeries { yName, title, color, - seriesId, + seriesId: this.id, ...this.getModuleTooltipParams(), } ); } getLegendData(legendType: ChartLegendType): CategoryLegendDatum[] { - const { id, data, xKey, yKey, yName, visible, title, marker, stroke, strokeOpacity } = this; - - if (!(data?.length && xKey && yKey && legendType === 'category')) { + if (!(this.data?.length && this.properties.isValid() && legendType === 'category')) { return []; } + const { yKey, yName, stroke, strokeOpacity, title, marker, visible } = this.properties; + return [ { legendType: 'category', - id: id, + id: this.id, itemId: yKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: title ?? yName ?? yKey, @@ -547,7 +492,7 @@ export class LineSeries extends CartesianSeries { } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } override getBandScalePadding() { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/lineSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/cartesian/lineSeriesProperties.ts new file mode 100644 index 0000000000..4fae6374f4 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/cartesian/lineSeriesProperties.ts @@ -0,0 +1,53 @@ +import type { + AgLineSeriesLabelFormatterParams, + AgLineSeriesOptions, + AgLineSeriesOptionsKeys, + AgLineSeriesTooltipRendererParams, +} from '../../../options/agChartOptions'; +import { COLOR_STRING, LINE_DASH, OBJECT, POSITIVE_NUMBER, RATIO, STRING, Validate } from '../../../util/validation'; +import { Label } from '../../label'; +import { SeriesMarker } from '../seriesMarker'; +import { SeriesTooltip } from '../seriesTooltip'; +import { CartesianSeriesProperties } from './cartesianSeries'; +import type { LineNodeDatum } from './lineSeries'; + +export class LineSeriesProperties extends CartesianSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + title?: string; + + @Validate(COLOR_STRING) + stroke: string = '#874349'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 2; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(OBJECT) + readonly marker = new SeriesMarker(); + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-community/src/chart/series/cartesian/scatterSeries.ts b/packages/ag-charts-community/src/chart/series/cartesian/scatterSeries.ts index e18ab1ad77..5a7d90d76c 100644 --- a/packages/ag-charts-community/src/chart/series/cartesian/scatterSeries.ts +++ b/packages/ag-charts-community/src/chart/series/cartesian/scatterSeries.ts @@ -1,9 +1,4 @@ import type { ModuleContext } from '../../../module/moduleContext'; -import type { - AgScatterSeriesLabelFormatterParams, - AgScatterSeriesOptionsKeys, - AgScatterSeriesTooltipRendererParams, -} from '../../../options/agChartOptions'; import { ColorScale } from '../../../scale/colorScale'; import { HdpiCanvas } from '../../../scene/canvas/hdpiCanvas'; import { Group } from '../../../scene/group'; @@ -14,24 +9,21 @@ import { extent } from '../../../util/array'; import type { MeasuredLabel, PointLabelDatum } from '../../../util/labelPlacement'; import { mergeDefaults } from '../../../util/object'; import { sanitizeHtml } from '../../../util/sanitize'; -import { COLOR_STRING_ARRAY, NUMBER_ARRAY, STRING, Validate } from '../../../util/validation'; import { ChartAxisDirection } from '../../chartAxisDirection'; import type { DataController } from '../../data/dataController'; import { fixNumericExtent } from '../../data/dataModel'; -import { Label } from '../../label'; import type { CategoryLegendDatum, ChartLegendType } from '../../legendDatum'; import type { Marker } from '../../marker/marker'; import { getMarker } from '../../marker/util'; import { SeriesNodePickMode, keyProperty, valueProperty } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation } from '../seriesLabelUtil'; -import { SeriesMarker } from '../seriesMarker'; -import { SeriesTooltip } from '../seriesTooltip'; import type { ErrorBoundSeriesNodeDatum } from '../seriesTypes'; import type { CartesianAnimationData, CartesianSeriesNodeDatum } from './cartesianSeries'; import { CartesianSeries } from './cartesianSeries'; import { markerScaleInAnimation, resetMarkerFn } from './markerUtil'; +import { ScatterSeriesProperties } from './scatterSeriesProperties'; -interface ScatterNodeDatum extends Required, ErrorBoundSeriesNodeDatum { +export interface ScatterNodeDatum extends Required, ErrorBoundSeriesNodeDatum { readonly label: MeasuredLabel; readonly fill: string | undefined; } @@ -42,46 +34,9 @@ export class ScatterSeries extends CartesianSeries { static className = 'ScatterSeries'; static type = 'scatter' as const; - readonly marker = new SeriesMarker(); + override properties = new ScatterSeriesProperties(); - readonly label = new Label(); - - @Validate(STRING, { optional: true }) - title?: string = undefined; - - @Validate(STRING, { optional: true }) - labelKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(STRING, { optional: true }) - labelName?: string = 'Label'; - - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorName?: string = 'Color'; - - @Validate(NUMBER_ARRAY, { optional: true }) - colorDomain?: number[]; - - @Validate(COLOR_STRING_ARRAY) - colorRange: string[] = ['#ffff00', '#00ff00', '#0000ff']; - - colorScale = new ColorScale(); - - readonly tooltip = new SeriesTooltip(); + readonly colorScale = new ColorScale(); constructor(moduleCtx: ModuleContext) { super({ @@ -102,14 +57,14 @@ export class ScatterSeries extends CartesianSeries { } override async processData(dataController: DataController) { - const { xKey, yKey, labelKey, data } = this; - - if (xKey == null || yKey == null || data == null) return; + if (!this.properties.isValid() || this.data == null) { + return; + } const { isContinuousX, isContinuousY } = this.isContinuous(); - const { colorScale, colorDomain, colorRange, colorKey } = this; + const { xKey, yKey, labelKey, colorKey, colorDomain, colorRange } = this.properties; - const { dataModel, processedData } = await this.requestDataModel(dataController, data, { + const { dataModel, processedData } = await this.requestDataModel(dataController, this.data, { props: [ keyProperty(this, xKey, isContinuousX, { id: 'xKey-raw' }), keyProperty(this, yKey, isContinuousY, { id: 'yKey-raw' }), @@ -124,9 +79,9 @@ export class ScatterSeries extends CartesianSeries { if (colorKey) { const colorKeyIdx = dataModel.resolveProcessedDataIndexById(this, `colorValue`).index; - colorScale.domain = colorDomain ?? processedData.domain.values[colorKeyIdx] ?? []; - colorScale.range = colorRange; - colorScale.update(); + this.colorScale.domain = colorDomain ?? processedData.domain.values[colorKeyIdx] ?? []; + this.colorScale.range = colorRange; + this.colorScale.update(); } this.animationState.transition('updateData'); @@ -147,25 +102,25 @@ export class ScatterSeries extends CartesianSeries { } async createNodeData() { - const { visible, axes, yKey = '', xKey = '', label, labelKey, dataModel, processedData } = this; + const { axes, dataModel, processedData, colorScale } = this; + const { xKey, yKey, labelKey, colorKey, xName, yName, labelName, marker, label, visible } = this.properties; const xAxis = axes[ChartAxisDirection.X]; const yAxis = axes[ChartAxisDirection.Y]; - if (!(dataModel && processedData && visible && xAxis && yAxis)) return []; + if (!(dataModel && processedData && visible && xAxis && yAxis)) { + return []; + } const xDataIdx = dataModel.resolveProcessedDataIndexById(this, `xValue`).index; const yDataIdx = dataModel.resolveProcessedDataIndexById(this, `yValue`).index; - const colorDataIdx = this.colorKey ? dataModel.resolveProcessedDataIndexById(this, `colorValue`).index : -1; - const labelDataIdx = this.labelKey ? dataModel.resolveProcessedDataIndexById(this, `labelValue`).index : -1; - - const { colorScale, colorKey } = this; + const colorDataIdx = colorKey ? dataModel.resolveProcessedDataIndexById(this, `colorValue`).index : -1; + const labelDataIdx = labelKey ? dataModel.resolveProcessedDataIndexById(this, `labelValue`).index : -1; const xScale = xAxis.scale; const yScale = yAxis.scale; const xOffset = (xScale.bandwidth ?? 0) / 2; const yOffset = (yScale.bandwidth ?? 0) / 2; - const { marker } = this; const nodeData: ScatterNodeDatum[] = []; const font = label.getFont(); @@ -175,15 +130,15 @@ export class ScatterSeries extends CartesianSeries { const x = xScale.convert(xDatum) + xOffset; const y = yScale.convert(yDatum) + yOffset; - const labelText = this.getLabelText(this.label, { + const labelText = this.getLabelText(label, { value: labelKey ? values[labelDataIdx] : yDatum, datum, xKey, yKey, labelKey, - xName: this.xName, - yName: this.yName, - labelName: this.labelName, + xName, + yName, + labelName, }); const size = HdpiCanvas.getTextSize(labelText, font); @@ -197,7 +152,7 @@ export class ScatterSeries extends CartesianSeries { datum, xValue: xDatum, yValue: yDatum, - capDefaults: { lengthRatioMultiplier: this.marker.getDiameter(), lengthMax: Infinity }, + capDefaults: { lengthRatioMultiplier: marker.getDiameter(), lengthMax: Infinity }, point: { x, y, size: marker.size }, midPoint: { x, y }, fill, @@ -207,7 +162,7 @@ export class ScatterSeries extends CartesianSeries { return [ { - itemId: this.yKey ?? this.id, + itemId: yKey, nodeData, labelData: nodeData, scales: super.calculateScaling(), @@ -217,7 +172,7 @@ export class ScatterSeries extends CartesianSeries { } protected override isPathOrSelectionDirty(): boolean { - return this.marker.isDirty(); + return this.properties.marker.isDirty(); } override getLabelData(): PointLabelDatum[] { @@ -225,7 +180,7 @@ export class ScatterSeries extends CartesianSeries { } protected override markerFactory() { - const { shape } = this.marker; + const { shape } = this.properties.marker; const MarkerShape = getMarker(shape); return new MarkerShape(); } @@ -235,33 +190,29 @@ export class ScatterSeries extends CartesianSeries { markerSelection: Selection; }) { const { nodeData, markerSelection } = opts; - const { - marker: { enabled }, - } = this; - if (this.marker.isDirty()) { + if (this.properties.marker.isDirty()) { markerSelection.clear(); markerSelection.cleanup(); } - const data = enabled ? nodeData : []; - return markerSelection.update(data); + return markerSelection.update(this.properties.marker.enabled ? nodeData : []); } protected override async updateMarkerNodes(opts: { markerSelection: Selection; isHighlight: boolean; }) { - const { xKey = '', yKey = '', labelKey, marker } = this; const { markerSelection, isHighlight: highlighted } = opts; - const baseStyle = mergeDefaults(highlighted && this.highlightStyle.item, marker.getStyle()); + const { xKey, yKey, labelKey, marker, highlightStyle } = this.properties; + const baseStyle = mergeDefaults(highlighted && highlightStyle.item, marker.getStyle()); markerSelection.each((node, datum) => { this.updateMarkerStyle(node, marker, { datum, highlighted, xKey, yKey, labelKey }, baseStyle); }); if (!highlighted) { - this.marker.markClean(); + marker.markClean(); } } @@ -269,33 +220,22 @@ export class ScatterSeries extends CartesianSeries { labelData: ScatterNodeDatum[]; labelSelection: Selection; }) { - const { labelSelection } = opts; - const { - label: { enabled }, - } = this; - - const placedLabels = enabled ? this.chart?.placeLabels().get(this) ?? [] : []; - - const placedNodeDatum = placedLabels.map( - (v): ScatterNodeDatum => ({ - ...(v.datum as ScatterNodeDatum), - point: { - x: v.x, - y: v.y, - size: v.datum.point.size, - }, - }) + const placedLabels = this.isLabelEnabled() ? this.chart?.placeLabels().get(this) ?? [] : []; + return opts.labelSelection.update( + placedLabels.map(({ datum, x, y }) => ({ + ...(datum as ScatterNodeDatum), + point: { x, y, size: datum.point.size }, + })), + (text) => { + text.pointerEvents = PointerEvents.None; + } ); - return labelSelection.update(placedNodeDatum, (text) => { - text.pointerEvents = PointerEvents.None; - }); } protected async updateLabelNodes(opts: { labelSelection: Selection }) { - const { labelSelection } = opts; - const { label } = this; + const { label } = this.properties; - labelSelection.each((text, datum) => { + opts.labelSelection.each((text, datum) => { text.text = datum.label.text; text.fill = label.color; text.x = datum.point?.x ?? 0; @@ -310,16 +250,15 @@ export class ScatterSeries extends CartesianSeries { } getTooltipHtml(nodeDatum: ScatterNodeDatum): string { - const { xKey, yKey, axes } = this; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; - - if (!xKey || !yKey || !xAxis || !yAxis) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { marker, tooltip, xName, yName, labelKey, labelName, id: seriesId, title = yName } = this; + const { xKey, yKey, labelKey, xName, yName, labelName, title = yName, marker, tooltip } = this.properties; + const { datum, xValue, yValue, label } = nodeDatum; const baseStyle = mergeDefaults( { fill: nodeDatum.fill, strokeWidth: this.getStrokeWidth(marker.strokeWidth) }, @@ -332,7 +271,6 @@ export class ScatterSeries extends CartesianSeries { baseStyle ); - const { datum, xValue, yValue, label } = nodeDatum; const xString = sanitizeHtml(xAxis.formatDatum(xValue)); const yString = sanitizeHtml(yAxis.formatDatum(yValue)); @@ -356,26 +294,26 @@ export class ScatterSeries extends CartesianSeries { labelName, title, color, - seriesId, + seriesId: this.id, ...this.getModuleTooltipParams(), } ); } getLegendData(legendType: ChartLegendType): CategoryLegendDatum[] { - const { id, data, xKey, yKey, yName, title, visible, marker } = this; + const { yKey, yName, title, marker, visible } = this.properties; const { fill, stroke, fillOpacity, strokeOpacity, strokeWidth } = marker; - if (!(data?.length && xKey && yKey && legendType === 'category')) { + if (!this.data?.length || !this.properties.isValid() || legendType !== 'category') { return []; } return [ { legendType: 'category', - id, + id: this.id, itemId: yKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: title ?? yName ?? yKey, @@ -400,7 +338,7 @@ export class ScatterSeries extends CartesianSeries { } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } protected nodeFactory() { diff --git a/packages/ag-charts-community/src/chart/series/cartesian/scatterSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/cartesian/scatterSeriesProperties.ts new file mode 100644 index 0000000000..cfa4fa1815 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/cartesian/scatterSeriesProperties.ts @@ -0,0 +1,56 @@ +import type { + AgScatterSeriesLabelFormatterParams, + AgScatterSeriesOptions, + AgScatterSeriesOptionsKeys, + AgScatterSeriesTooltipRendererParams, +} from '../../../options/agChartOptions'; +import { COLOR_STRING_ARRAY, NUMBER_ARRAY, OBJECT, STRING, Validate } from '../../../util/validation'; +import { Label } from '../../label'; +import { SeriesMarker } from '../seriesMarker'; +import { SeriesTooltip } from '../seriesTooltip'; +import { CartesianSeriesProperties } from './cartesianSeries'; +import type { ScatterNodeDatum } from './scatterSeries'; + +export class ScatterSeriesProperties extends CartesianSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING, { optional: true }) + labelKey?: string; + + @Validate(STRING, { optional: true }) + colorKey?: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + labelName?: string; + + @Validate(STRING, { optional: true }) + colorName?: string; + + @Validate(NUMBER_ARRAY, { optional: true }) + colorDomain?: number[]; + + @Validate(COLOR_STRING_ARRAY) + colorRange: string[] = ['#ffff00', '#00ff00', '#0000ff']; + + @Validate(STRING, { optional: true }) + title?: string; + + @Validate(OBJECT) + readonly marker = new SeriesMarker(); + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-community/src/chart/series/dataModelSeries.ts b/packages/ag-charts-community/src/chart/series/dataModelSeries.ts index b3bbf9d18f..72ed2448ef 100644 --- a/packages/ag-charts-community/src/chart/series/dataModelSeries.ts +++ b/packages/ag-charts-community/src/chart/series/dataModelSeries.ts @@ -1,7 +1,7 @@ import { ContinuousScale } from '../../scale/continuousScale'; import { ChartAxisDirection } from '../chartAxisDirection'; import type { DataController } from '../data/dataController'; -import type { DataModel, DataModelOptions, ProcessedData } from '../data/dataModel'; +import type { DataModel, DataModelOptions, ProcessedData, PropertyDefinition } from '../data/dataModel'; import type { SeriesNodeDataContext } from './series'; import { Series } from './series'; import type { SeriesNodeDatum } from './seriesTypes'; @@ -32,14 +32,9 @@ export abstract class DataModelSeries< G extends boolean | undefined = undefined, >(dataController: DataController, data: D[] | undefined, opts: DataModelOptions) { // Merge properties of this series with properties of all the attached series-options - const props = opts.props; - const moduleProps = this.getModulePropertyDefinitions() as typeof props; - props.push(...moduleProps); + opts.props.push(...(this.getModulePropertyDefinitions() as PropertyDefinition[])); - const { dataModel, processedData } = await dataController.request(this.id, data ?? [], { - ...opts, - props, - }); + const { dataModel, processedData } = await dataController.request(this.id, data ?? [], opts); this.dataModel = dataModel; this.processedData = processedData; @@ -49,10 +44,12 @@ export abstract class DataModelSeries< protected isProcessedDataAnimatable() { const validationResults = this.processedData?.reduced?.animationValidation; - if (!validationResults) return true; + if (!validationResults) { + return true; + } const { orderedKeys, uniqueKeys } = validationResults; - return !!orderedKeys && !!uniqueKeys; + return orderedKeys && uniqueKeys; } protected checkProcessedDataAnimatable() { diff --git a/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.test.ts b/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.test.ts index 501f9c76e8..9a2a0f266e 100644 --- a/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.test.ts +++ b/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.test.ts @@ -4,9 +4,14 @@ import type { ChartLegendDatum, ChartLegendType } from '../../legendDatum'; import type { SeriesNodeDataContext } from '../series'; import type { SeriesTooltip } from '../seriesTooltip'; import { HierarchySeries } from './hierarchySeries'; +import { HierarchySeriesProperties } from './hierarchySeriesProperties'; + +class ExampleHierarchySeriesProperties extends HierarchySeriesProperties { + readonly tooltip: SeriesTooltip = null!; +} class ExampleHierarchySeries extends HierarchySeries { - override tooltip: SeriesTooltip = null!; + override properties = new ExampleHierarchySeriesProperties(); override getSeriesDomain(_direction: ChartAxisDirection): any[] { throw new Error('Method not implemented.'); @@ -32,7 +37,7 @@ class ExampleHierarchySeries extends HierarchySeries { describe('HierarchySeries', () => { it('creates a hierarchy', async () => { const series = new ExampleHierarchySeries(null!); - series.sizeKey = 'size'; + series.properties.sizeKey = 'size'; series.data = [ { size: 5, children: [{ size: 1 }, { size: 2 }, { size: 3 }] }, { diff --git a/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.ts b/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.ts index 2d3f14237e..24183d62d0 100644 --- a/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.ts +++ b/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeries.ts @@ -8,12 +8,11 @@ import type { Group } from '../../../scene/group'; import type { Node } from '../../../scene/node'; import type { Selection } from '../../../scene/selection'; import type { PointLabelDatum } from '../../../util/labelPlacement'; -import { COLOR_STRING_ARRAY, STRING, Validate } from '../../../util/validation'; import type { HighlightNodeDatum } from '../../interaction/highlightManager'; import type { ChartLegendType, GradientLegendDatum } from '../../legendDatum'; -import { DEFAULT_FILLS, DEFAULT_STROKES } from '../../themes/defaultColors'; import { Series, SeriesNodePickMode } from '../series'; import type { ISeries, SeriesNodeDatum } from '../seriesTypes'; +import type { HierarchySeriesProperties } from './hierarchySeriesProperties'; type Mutable = { -readonly [k in keyof T]: T[k]; @@ -91,26 +90,7 @@ export abstract class HierarchySeries< TNode extends Node = Group, TDatum extends SeriesNodeDatum = SeriesNodeDatum, > extends Series { - @Validate(STRING, { optional: true }) - childrenKey?: string = 'children'; - - @Validate(STRING, { optional: true }) - sizeKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorName?: string = undefined; - - @Validate(COLOR_STRING_ARRAY, { optional: true }) - fills: string[] = Object.values(DEFAULT_FILLS); - - @Validate(COLOR_STRING_ARRAY, { optional: true }) - strokes: string[] = Object.values(DEFAULT_STROKES); - - @Validate(COLOR_STRING_ARRAY, { optional: true }) - colorRange?: string[] = undefined; + abstract override properties: HierarchySeriesProperties; rootNode = new HierarchyNode( this, @@ -180,7 +160,7 @@ export abstract class HierarchySeries< } override async processData(): Promise { - const { childrenKey, sizeKey, colorKey, fills, strokes, colorRange } = this; + const { childrenKey, sizeKey, colorKey, fills, strokes, colorRange } = this.properties; let index = 0; const getIndex = () => { @@ -378,15 +358,16 @@ export abstract class HierarchySeries< } override getLegendData(legendType: ChartLegendType): GradientLegendDatum[] { - return legendType === 'gradient' && this.colorKey != null && this.colorRange != null + const { colorKey, colorName, colorRange, visible } = this.properties; + return legendType === 'gradient' && colorKey != null && colorRange != null ? [ { legendType: 'gradient', - enabled: this.visible, + enabled: visible, seriesId: this.id, - colorName: this.colorName, + colorName, + colorRange, colorDomain: this.colorDomain, - colorRange: this.colorRange, }, ] : []; diff --git a/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeriesProperties.ts b/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeriesProperties.ts new file mode 100644 index 0000000000..a4ff3bf223 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/hierarchy/hierarchySeriesProperties.ts @@ -0,0 +1,26 @@ +import { COLOR_STRING_ARRAY, STRING, Validate } from '../../../util/validation'; +import { DEFAULT_FILLS, DEFAULT_STROKES } from '../../themes/defaultColors'; +import { SeriesProperties } from '../seriesProperties'; + +export abstract class HierarchySeriesProperties extends SeriesProperties { + @Validate(STRING) + childrenKey: string = 'children'; + + @Validate(STRING, { optional: true }) + sizeKey?: string; + + @Validate(STRING, { optional: true }) + colorKey?: string; + + @Validate(STRING, { optional: true }) + colorName?: string; + + @Validate(COLOR_STRING_ARRAY) + fills: string[] = Object.values(DEFAULT_FILLS); + + @Validate(COLOR_STRING_ARRAY) + strokes: string[] = Object.values(DEFAULT_STROKES); + + @Validate(COLOR_STRING_ARRAY, { optional: true }) + colorRange?: string[]; +} diff --git a/packages/ag-charts-community/src/chart/series/polar/__image_snapshots__/PolarSeries no series cases for PIE_SERIES it should render identically after legend toggle-diff.png b/packages/ag-charts-community/src/chart/series/polar/__image_snapshots__/PolarSeries no series cases for PIE_SERIES it should render identically after legend toggle-diff.png deleted file mode 100644 index 29b89119c6..0000000000 Binary files a/packages/ag-charts-community/src/chart/series/polar/__image_snapshots__/PolarSeries no series cases for PIE_SERIES it should render identically after legend toggle-diff.png and /dev/null differ diff --git a/packages/ag-charts-community/src/chart/series/polar/pieSeries.ts b/packages/ag-charts-community/src/chart/series/polar/pieSeries.ts index 7337192ef4..8b0dbdf0b0 100644 --- a/packages/ag-charts-community/src/chart/series/polar/pieSeries.ts +++ b/packages/ag-charts-community/src/chart/series/polar/pieSeries.ts @@ -1,14 +1,8 @@ import type { ModuleContext } from '../../../module/moduleContext'; import { fromToMotion } from '../../../motion/fromToMotion'; -import type { - AgPieSeriesFormat, - AgPieSeriesFormatterParams, - AgPieSeriesLabelFormatterParams, - AgPieSeriesTooltipRendererParams, -} from '../../../options/agChartOptions'; +import type { AgPieSeriesFormat } from '../../../options/agChartOptions'; import { LinearScale } from '../../../scale/linearScale'; import { BBox } from '../../../scene/bbox'; -import type { DropShadow } from '../../../scene/dropShadow'; import { Group } from '../../../scene/group'; import { PointerEvents } from '../../../scene/node'; import { Selection } from '../../../scene/selection'; @@ -23,34 +17,17 @@ import { mergeDefaults } from '../../../util/object'; import { sanitizeHtml } from '../../../util/sanitize'; import { boxCollidesSector, isPointInSector } from '../../../util/sector'; import type { Has } from '../../../util/types'; -import { - BOOLEAN, - COLOR_STRING, - COLOR_STRING_ARRAY, - DEGREE, - FUNCTION, - LINE_DASH, - NUMBER, - POSITIVE_NUMBER, - RATIO, - STRING, - Validate, -} from '../../../util/validation'; import { isNumber } from '../../../util/value'; -import { Caption } from '../../caption'; import { ChartAxisDirection } from '../../chartAxisDirection'; import type { DataController } from '../../data/dataController'; import type { DataModel } from '../../data/dataModel'; import { animationValidation, diff, normalisePropertyTo } from '../../data/processors'; import type { LegendItemClickChartEvent } from '../../interaction/chartEventManager'; -import { Label } from '../../label'; import { Layers } from '../../layers'; import type { CategoryLegendDatum, ChartLegendType } from '../../legendDatum'; import { Circle } from '../../marker/circle'; -import { DEFAULT_FILLS, DEFAULT_STROKES } from '../../themes/defaultColors'; import type { SeriesNodeEventTypes } from '../series'; import { - HighlightStyle, SeriesNodeClickEvent, accumulativeValueProperty, keyProperty, @@ -58,8 +35,9 @@ import { valueProperty, } from '../series'; import { resetLabelFn, seriesLabelFadeInAnimation, seriesLabelFadeOutAnimation } from '../seriesLabelUtil'; -import { SeriesTooltip } from '../seriesTooltip'; import type { SeriesNodeDatum } from '../seriesTypes'; +import type { DoughnutInnerLabel, PieTitle } from './pieSeriesProperties'; +import { PieSeriesProperties } from './pieSeriesProperties'; import { preparePieSeriesAnimationFunctions, resetPieSelectionsFn } from './pieUtil'; import { type PolarAnimationData, PolarSeries } from './polarSeries'; @@ -73,10 +51,10 @@ class PieSeriesNodeClickEvent exte readonly sectorLabelKey?: string; constructor(type: TEvent, nativeEvent: MouseEvent, datum: PieNodeDatum, series: PieSeries) { super(type, nativeEvent, datum, series); - this.angleKey = series.angleKey; - this.radiusKey = series.radiusKey; - this.calloutLabelKey = series.calloutLabelKey; - this.sectorLabelKey = series.sectorLabelKey; + this.angleKey = series.properties.angleKey; + this.radiusKey = series.properties.radiusKey; + this.calloutLabelKey = series.properties.calloutLabelKey; + this.sectorLabelKey = series.properties.sectorLabelKey; } } @@ -118,69 +96,12 @@ enum PieNodeTag { Label, } -class PieSeriesCalloutLabel extends Label { - @Validate(POSITIVE_NUMBER) - offset = 3; // from the callout line - - @Validate(POSITIVE_NUMBER) - minAngle = 0; // in degrees - - @Validate(POSITIVE_NUMBER) - minSpacing = 4; - - @Validate(POSITIVE_NUMBER) - maxCollisionOffset = 50; - - @Validate(BOOLEAN) - avoidCollisions = true; -} - -class PieSeriesSectorLabel extends Label { - @Validate(NUMBER) - positionOffset = 0; - - @Validate(RATIO) - positionRatio = 0.5; -} - -class PieSeriesCalloutLine { - @Validate(COLOR_STRING_ARRAY, { optional: true }) - colors?: string[]; - - @Validate(POSITIVE_NUMBER) - length: number = 10; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; -} - -export class PieTitle extends Caption { - @Validate(BOOLEAN) - showInLegend = false; - - constructor(moduleCtx: ModuleContext) { - super(moduleCtx); - } -} - -export class DoughnutInnerLabel extends Label { - @Validate(STRING) - text = ''; - @Validate(NUMBER) - margin = 2; -} - -export class DoughnutInnerCircle { - @Validate(COLOR_STRING) - fill = 'transparent'; - @Validate(RATIO, { optional: true }) - fillOpacity? = 1; -} - export class PieSeries extends PolarSeries { static className = 'PieSeries'; static type = 'pie' as const; + override properties = new PieSeriesProperties(); + private readonly previousRadiusScale: LinearScale = new LinearScale(); private readonly radiusScale: LinearScale = new LinearScale(); private readonly calloutLabelSelection: Selection; @@ -210,112 +131,8 @@ export class PieSeries extends PolarSeries { // When a user toggles a series item (e.g. from the legend), its boolean state is recorded here. public seriesItemEnabled: boolean[] = []; - title?: PieTitle = undefined; private oldTitle?: PieTitle; - calloutLabel = new PieSeriesCalloutLabel(); - - readonly sectorLabel = new PieSeriesSectorLabel(); - - calloutLine = new PieSeriesCalloutLine(); - - tooltip = new SeriesTooltip(); - - /** - * The key of the numeric field to use to determine the angle (for example, - * a pie sector angle). - */ - @Validate(STRING) - angleKey = ''; - - @Validate(STRING) - angleName = ''; - - readonly innerLabels: DoughnutInnerLabel[] = []; - - innerCircle?: DoughnutInnerCircle = undefined; - - /** - * The key of the numeric field to use to determine the radii of pie sectors. - * The largest value will correspond to the full radius and smaller values to - * proportionally smaller radii. - */ - @Validate(STRING, { optional: true }) - radiusKey?: string = undefined; - - @Validate(STRING, { optional: true }) - radiusName?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - radiusMin?: number = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - radiusMax?: number = undefined; - - @Validate(STRING, { optional: true }) - calloutLabelKey?: string = undefined; - - @Validate(STRING, { optional: true }) - calloutLabelName?: string = undefined; - - @Validate(STRING, { optional: true }) - sectorLabelKey?: string = undefined; - - @Validate(STRING, { optional: true }) - sectorLabelName?: string = undefined; - - @Validate(STRING, { optional: true }) - legendItemKey?: string = undefined; - - @Validate(COLOR_STRING_ARRAY) - fills: string[] = Object.values(DEFAULT_FILLS); - - @Validate(COLOR_STRING_ARRAY) - strokes: string[] = Object.values(DEFAULT_STROKES); - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgPieSeriesFormatterParams) => AgPieSeriesFormat = undefined; - - /** - * The series rotation in degrees. - */ - @Validate(DEGREE) - rotation = 0; - - @Validate(NUMBER) - outerRadiusOffset = 0; - - @Validate(POSITIVE_NUMBER) - outerRadiusRatio = 1; - - @Validate(NUMBER) - innerRadiusOffset = 0; - - @Validate(POSITIVE_NUMBER) - innerRadiusRatio = 1; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 1; - - @Validate(POSITIVE_NUMBER) - sectorSpacing = 1; - - shadow?: DropShadow = undefined; - - override readonly highlightStyle = new HighlightStyle(); - surroundingRadius?: number = undefined; constructor(moduleCtx: ModuleContext) { @@ -344,7 +161,7 @@ export class PieSeries extends PolarSeries { for (const circle of [this.zerosumInnerRing, this.zerosumOuterRing]) { circle.fillOpacity = 0; - circle.stroke = this.calloutLabel.color; + circle.stroke = this.properties.calloutLabel.color; circle.strokeWidth = 1; circle.strokeOpacity = 1; } @@ -376,10 +193,13 @@ export class PieSeries extends PolarSeries { } override async processData(dataController: DataController) { - let { data } = this; - const { angleKey, radiusKey, calloutLabelKey, sectorLabelKey, legendItemKey, seriesItemEnabled } = this; + if (this.data == null || !this.properties.isValid()) { + return; + } - if (angleKey == null || data == null) return; + let { data } = this; + const { seriesItemEnabled } = this; + const { angleKey, radiusKey, calloutLabelKey, sectorLabelKey, legendItemKey } = this.properties; const animationEnabled = !this.ctx.animationManager.isSkipped(); const extraKeyProps = []; @@ -398,11 +218,18 @@ export class PieSeries extends PolarSeries { extraProps.push( rangedValueProperty(this, radiusKey, { id: 'radiusValue', - min: this.radiusMin ?? 0, - max: this.radiusMax, + min: this.properties.radiusMin ?? 0, + max: this.properties.radiusMax, }), valueProperty(this, radiusKey, true, { id: `radiusRaw` }), // Raw value pass-through. - normalisePropertyTo(this, { id: 'radiusValue' }, [0, 1], 1, this.radiusMin ?? 0, this.radiusMax) + normalisePropertyTo( + this, + { id: 'radiusValue' }, + [0, 1], + 1, + this.properties.radiusMin ?? 0, + this.properties.radiusMax + ) ); } if (calloutLabelKey) { @@ -458,14 +285,16 @@ export class PieSeries extends PolarSeries { private getProcessedDataIndexes(dataModel: DataModel) { const angleIdx = dataModel.resolveProcessedDataIndexById(this, `angleValue`).index; - const radiusIdx = this.radiusKey ? dataModel.resolveProcessedDataIndexById(this, `radiusValue`).index : -1; - const calloutLabelIdx = this.calloutLabelKey + const radiusIdx = this.properties.radiusKey + ? dataModel.resolveProcessedDataIndexById(this, `radiusValue`).index + : -1; + const calloutLabelIdx = this.properties.calloutLabelKey ? dataModel.resolveProcessedDataIndexById(this, `calloutLabelValue`).index : -1; - const sectorLabelIdx = this.sectorLabelKey + const sectorLabelIdx = this.properties.sectorLabelKey ? dataModel.resolveProcessedDataIndexById(this, `sectorLabelValue`).index : -1; - const legendItemIdx = this.legendItemKey + const legendItemIdx = this.properties.legendItemKey ? dataModel.resolveProcessedDataIndexById(this, `legendItemValue`).index : -1; @@ -473,7 +302,8 @@ export class PieSeries extends PolarSeries { } async createNodeData() { - const { id: seriesId, processedData, dataModel, rotation, angleScale } = this; + const { id: seriesId, processedData, dataModel, angleScale } = this; + const { rotation } = this.properties; if (!processedData || !dataModel || processedData.type !== 'ungrouped') return []; @@ -531,7 +361,8 @@ export class PieSeries extends PolarSeries { }); this.zerosumOuterRing.visible = sum === 0; - this.zerosumInnerRing.visible = sum === 0 && this.innerRadiusRatio !== 1 && this.innerRadiusRatio > 0; + this.zerosumInnerRing.visible = + sum === 0 && this.properties.innerRadiusRatio !== 1 && this.properties.innerRadiusRatio > 0; return [{ itemId: seriesId, nodeData, labelData: nodeData }]; } @@ -545,10 +376,10 @@ export class PieSeries extends PolarSeries { sectorLabelValue: string, legendItemValue?: string ) { - const { calloutLabel, sectorLabel, legendItemKey } = this; + const { calloutLabel, sectorLabel, legendItemKey } = this.properties; - const calloutLabelKey = !skipDisabled || calloutLabel.enabled ? this.calloutLabelKey : undefined; - const sectorLabelKey = !skipDisabled || sectorLabel.enabled ? this.sectorLabelKey : undefined; + const calloutLabelKey = !skipDisabled || calloutLabel.enabled ? this.properties.calloutLabelKey : undefined; + const sectorLabelKey = !skipDisabled || sectorLabel.enabled ? this.properties.sectorLabelKey : undefined; if (!calloutLabelKey && !sectorLabelKey && !legendItemKey) { return {}; @@ -556,15 +387,15 @@ export class PieSeries extends PolarSeries { const labelFormatterParams = { datum, - angleKey: this.angleKey, - angleName: this.angleName, - radiusKey: this.radiusKey, - radiusName: this.radiusName, - calloutLabelKey: this.calloutLabelKey, - calloutLabelName: this.calloutLabelName, - sectorLabelKey: this.sectorLabelKey, - sectorLabelName: this.sectorLabelName, - legendItemKey: this.legendItemKey, + angleKey: this.properties.angleKey, + angleName: this.properties.angleName, + radiusKey: this.properties.radiusKey, + radiusName: this.properties.radiusName, + calloutLabelKey: this.properties.calloutLabelKey, + calloutLabelName: this.properties.calloutLabelName, + sectorLabelKey: this.properties.sectorLabelKey, + sectorLabelName: this.properties.sectorLabelName, + legendItemKey: this.properties.legendItemKey, }; const result: { @@ -623,27 +454,20 @@ export class PieSeries extends PolarSeries { } private getSectorFormat(datum: any, formatIndex: number, highlight: boolean) { - const { - angleKey, - radiusKey, - fills, - strokes, - formatter, - id: seriesId, - ctx: { callbackCache, highlightManager }, - } = this; + const { callbackCache, highlightManager } = this.ctx; + const { angleKey, radiusKey, fills, strokes, formatter } = this.properties; const highlightedDatum = highlightManager.getActiveHighlight(); const isDatumHighlighted = highlight && highlightedDatum?.series === this && formatIndex === highlightedDatum.itemId; const { fill, fillOpacity, stroke, strokeWidth, strokeOpacity } = mergeDefaults( - isDatumHighlighted && this.highlightStyle.item, + isDatumHighlighted && this.properties.highlightStyle.item, { fill: fills.length > 0 ? fills[formatIndex % fills.length] : undefined, - fillOpacity: this.fillOpacity, + fillOpacity: this.properties.fillOpacity, stroke: strokes.length > 0 ? strokes[formatIndex % strokes.length] : undefined, - strokeWidth: this.getStrokeWidth(this.strokeWidth), + strokeWidth: this.getStrokeWidth(this.properties.strokeWidth), strokeOpacity: this.getOpacity(), } ); @@ -660,7 +484,7 @@ export class PieSeries extends PolarSeries { strokes, strokeWidth, highlighted: isDatumHighlighted, - seriesId, + seriesId: this.id, }); } @@ -674,7 +498,8 @@ export class PieSeries extends PolarSeries { } getInnerRadius() { - const { radius, innerRadiusRatio, innerRadiusOffset } = this; + const { radius } = this; + const { innerRadiusRatio, innerRadiusOffset } = this.properties; const innerRadius = radius * innerRadiusRatio + innerRadiusOffset; if (innerRadius === radius || innerRadius < 0) { return 0; @@ -683,7 +508,7 @@ export class PieSeries extends PolarSeries { } getOuterRadius() { - return Math.max(this.radius * this.outerRadiusRatio + this.outerRadiusOffset, 0); + return Math.max(this.radius * this.properties.outerRadiusRatio + this.properties.outerRadiusOffset, 0); } updateRadiusScale(resize: boolean) { @@ -709,14 +534,14 @@ export class PieSeries extends PolarSeries { if (outerRadius === 0) { return NaN; } - const spacing = this.title?.spacing ?? 0; + const spacing = this.properties.title?.spacing ?? 0; const titleOffset = 2 + spacing; const dy = Math.max(0, -outerRadius); return -outerRadius - titleOffset - dy; } async update({ seriesRect }: { seriesRect: BBox }) { - const { title } = this; + const { title } = this.properties; const newNodeDataDependencies = { seriesRectWidth: seriesRect?.width, @@ -757,7 +582,8 @@ export class PieSeries extends PolarSeries { } private updateTitleNodes() { - const { title, oldTitle } = this; + const { oldTitle } = this; + const { title } = this.properties; if (oldTitle !== title) { if (oldTitle) { @@ -824,13 +650,13 @@ export class PieSeries extends PolarSeries { node.pointerEvents = PointerEvents.None; }); - innerLabelsSelection.update(this.innerLabels, (node) => { + innerLabelsSelection.update(this.properties.innerLabels, (node) => { node.pointerEvents = PointerEvents.None; }); } private updateInnerCircleSelection() { - const { innerCircle } = this; + const { innerCircle } = this.properties; let radius = 0; const innerRadius = this.getInnerRadius(); @@ -859,8 +685,8 @@ export class PieSeries extends PolarSeries { this.innerCircleSelection.each((node, { radius }) => { node.setProperties({ - fill: this.innerCircle?.fill, - opacity: this.innerCircle?.fillOpacity, + fill: this.properties.innerCircle?.fill, + opacity: this.properties.innerCircle?.fillOpacity, size: radius, }); }); @@ -885,11 +711,11 @@ export class PieSeries extends PolarSeries { sector.strokeWidth = format.strokeWidth!; sector.fillOpacity = format.fillOpacity!; - sector.strokeOpacity = this.strokeOpacity; - sector.lineDash = this.lineDash; - sector.lineDashOffset = this.lineDashOffset; - sector.fillShadow = this.shadow; - sector.inset = (this.sectorSpacing + (format.stroke != null ? format.strokeWidth! : 0)) / 2; + sector.strokeOpacity = this.properties.strokeOpacity; + sector.lineDash = this.properties.lineDash; + sector.lineDashOffset = this.properties.lineDashOffset; + sector.fillShadow = this.properties.shadow; + sector.inset = (this.properties.sectorSpacing + (format.stroke != null ? format.strokeWidth! : 0)) / 2; }; this.itemSelection.each((node, datum, index) => updateSectorFn(node, datum, index, false)); @@ -911,11 +737,11 @@ export class PieSeries extends PolarSeries { } updateCalloutLineNodes() { - const { calloutLine } = this; + const { calloutLine } = this.properties; const calloutLength = calloutLine.length; const calloutStrokeWidth = calloutLine.strokeWidth; - const calloutColors = calloutLine.colors ?? this.strokes; - const { offset } = this.calloutLabel; + const calloutColors = calloutLine.colors ?? this.properties.strokes; + const { offset } = this.properties.calloutLabel; this.calloutLabelSelection.selectByTag(PieNodeTag.Callout).forEach((line, index) => { const datum = line.datum as PieNodeDatum; @@ -1004,7 +830,8 @@ export class PieSeries extends PolarSeries { } private computeCalloutLabelCollisionOffsets() { - const { radiusScale, calloutLabel, calloutLine } = this; + const { radiusScale } = this; + const { calloutLabel, calloutLine } = this.properties; const { offset, minSpacing } = calloutLabel; const innerRadius = radiusScale.convert(0); @@ -1049,7 +876,7 @@ export class PieSeries extends PolarSeries { tempTextNode.text = label.text; tempTextNode.x = x; tempTextNode.y = y; - tempTextNode.setFont(this.calloutLabel); + tempTextNode.setFont(this.properties.calloutLabel); tempTextNode.setAlign({ textAlign: label.collisionTextAlign ?? label.textAlign, textBaseline: label.textBaseline, @@ -1142,7 +969,8 @@ export class PieSeries extends PolarSeries { } private updateCalloutLabelNodes(seriesRect: BBox) { - const { radiusScale, calloutLabel, calloutLine } = this; + const { radiusScale } = this; + const { calloutLabel, calloutLine } = this.properties; const calloutLength = calloutLine.length; const { offset, color } = calloutLabel; @@ -1171,7 +999,7 @@ export class PieSeries extends PolarSeries { tempTextNode.text = label.text; tempTextNode.x = x; tempTextNode.y = y; - tempTextNode.setFont(this.calloutLabel); + tempTextNode.setFont(this.properties.calloutLabel); tempTextNode.setAlign(align); const box = tempTextNode.computeBBox(); @@ -1186,7 +1014,7 @@ export class PieSeries extends PolarSeries { text.text = displayText; text.x = x; text.y = y; - text.setFont(this.calloutLabel); + text.setFont(this.properties.calloutLabel); text.setAlign(align); text.fill = color; text.visible = visible; @@ -1194,7 +1022,7 @@ export class PieSeries extends PolarSeries { } override async computeLabelsBBox(options: { hideWhenNecessary: boolean }, seriesRect: BBox) { - const { calloutLabel, calloutLine } = this; + const { calloutLabel, calloutLine } = this.properties; const calloutLength = calloutLine.length; const { offset, maxCollisionOffset, minSpacing } = calloutLabel; @@ -1211,13 +1039,14 @@ export class PieSeries extends PolarSeries { const text = new Text(); let titleBox: BBox; - if (this.title?.text && this.title.enabled) { + const { title } = this.properties; + if (title?.text && title.enabled) { const dy = this.getTitleTranslationY(); if (isFinite(dy)) { - text.text = this.title.text; + text.text = title.text; text.x = 0; text.y = dy; - text.setFont(this.title); + text.setFont(title); text.setAlign({ textBaseline: 'bottom', textAlign: 'center', @@ -1239,7 +1068,7 @@ export class PieSeries extends PolarSeries { text.text = label.text; text.x = x; text.y = y; - text.setFont(this.calloutLabel); + text.setFont(this.properties.calloutLabel); text.setAlign({ textAlign: label.collisionTextAlign ?? label.textAlign, textBaseline: label.textBaseline, @@ -1294,7 +1123,8 @@ export class PieSeries extends PolarSeries { private updateSectorLabelNodes() { const { radiusScale } = this; const innerRadius = radiusScale.convert(0); - const { fontSize, fontStyle, fontWeight, fontFamily, positionOffset, positionRatio, color } = this.sectorLabel; + const { fontSize, fontStyle, fontWeight, fontFamily, positionOffset, positionRatio, color } = + this.properties.sectorLabel; const isDoughnut = innerRadius > 0; const singleVisibleSector = this.seriesItemEnabled.filter(Boolean).length === 1; @@ -1390,7 +1220,7 @@ export class PieSeries extends PolarSeries { protected override readonly NodeClickEvent = PieSeriesNodeClickEvent; private getDatumLegendName(nodeDatum: PieNodeDatum) { - const { angleKey, calloutLabelKey, sectorLabelKey, legendItemKey } = this; + const { angleKey, calloutLabelKey, sectorLabelKey, legendItemKey } = this.properties; const { sectorLabel, calloutLabel, legendItem } = nodeDatum; if (legendItemKey && legendItem !== undefined) { @@ -1403,7 +1233,7 @@ export class PieSeries extends PolarSeries { } getTooltipHtml(nodeDatum: PieNodeDatum): string { - if (!this.angleKey) { + if (!this.properties.isValid()) { return ''; } @@ -1413,11 +1243,11 @@ export class PieSeries extends PolarSeries { sectorFormat: { fill: color }, } = nodeDatum; - const title = sanitizeHtml(this.title?.text); + const title = sanitizeHtml(this.properties.title?.text); const content = isNumber(angleValue) ? toFixed(angleValue) : String(angleValue); const labelText = this.getDatumLegendName(nodeDatum); - return this.tooltip.toTooltipHtml( + return this.properties.tooltip.toTooltipHtml( { title: title ?? labelText, content: title && labelText ? `${labelText}: ${content}` : content, @@ -1428,22 +1258,26 @@ export class PieSeries extends PolarSeries { title, color, seriesId: this.id, - angleKey: this.angleKey, - angleName: this.angleName, - radiusKey: this.radiusKey, - radiusName: this.radiusName, - calloutLabelKey: this.calloutLabelKey, - calloutLabelName: this.calloutLabelName, - sectorLabelKey: this.sectorLabelKey, - sectorLabelName: this.sectorLabelName, + angleKey: this.properties.angleKey, + angleName: this.properties.angleName, + radiusKey: this.properties.radiusKey, + radiusName: this.properties.radiusName, + calloutLabelKey: this.properties.calloutLabelKey, + calloutLabelName: this.properties.calloutLabelName, + sectorLabelKey: this.properties.sectorLabelKey, + sectorLabelName: this.properties.sectorLabelName, } ); } getLegendData(legendType: ChartLegendType): CategoryLegendDatum[] { - const { processedData, angleKey, calloutLabelKey, sectorLabelKey, legendItemKey, id, dataModel } = this; + const { processedData, dataModel } = this; - if (!dataModel || !processedData || processedData.data.length === 0 || legendType !== 'category') return []; + if (!dataModel || !processedData?.data.length || legendType !== 'category') { + return []; + } + + const { angleKey, calloutLabelKey, sectorLabelKey, legendItemKey } = this.properties; if ( !legendItemKey && @@ -1454,7 +1288,7 @@ export class PieSeries extends PolarSeries { const { calloutLabelIdx, sectorLabelIdx, legendItemIdx } = this.getProcessedDataIndexes(dataModel); - const titleText = this.title?.showInLegend && this.title.text; + const titleText = this.properties.title?.showInLegend && this.properties.title.text; const legendData: CategoryLegendDatum[] = []; for (let index = 0; index < processedData.data.length; index++) { @@ -1488,9 +1322,9 @@ export class PieSeries extends PolarSeries { legendData.push({ legendType: 'category', - id, + id: this.id, itemId: index, - seriesId: id, + seriesId: this.id, enabled: this.seriesItemEnabled[index], label: { text: labelParts.join(' - '), @@ -1498,9 +1332,9 @@ export class PieSeries extends PolarSeries { marker: { fill: sectorFormat.fill, stroke: sectorFormat.stroke, - fillOpacity: this.fillOpacity, - strokeOpacity: this.strokeOpacity, - strokeWidth: this.strokeWidth, + fillOpacity: this.properties.fillOpacity, + strokeOpacity: this.properties.strokeOpacity, + strokeWidth: this.properties.strokeWidth, }, }); } @@ -1524,16 +1358,19 @@ export class PieSeries extends PolarSeries { } toggleOtherSeriesItems(series: PieSeries, itemId: number, enabled: boolean): void { - const { legendItemKey, dataModel } = this; - - if (!legendItemKey || !dataModel) return; + if (!this.properties.legendItemKey || !this.dataModel) { + return; + } const datumToggledLegendItemValue = - series.legendItemKey && series.data?.find((_, index) => index === itemId)[series.legendItemKey]; + series.properties.legendItemKey && + series.data?.find((_, index) => index === itemId)[series.properties.legendItemKey]; - if (!datumToggledLegendItemValue) return; + if (!datumToggledLegendItemValue) { + return; + } - const legendItemIdx = dataModel.resolveProcessedDataIndexById(this, `legendItemValue`).index; + const legendItemIdx = this.dataModel.resolveProcessedDataIndexById(this, `legendItemValue`).index; this.processedData?.data.forEach(({ values }, datumItemId) => { if (values[legendItemIdx] === datumToggledLegendItemValue) { this.toggleSeriesItem(datumItemId, enabled); @@ -1544,7 +1381,12 @@ export class PieSeries extends PolarSeries { override animateEmptyUpdateReady(_data?: PolarAnimationData) { const { animationManager } = this.ctx; - const fns = preparePieSeriesAnimationFunctions(true, this.rotation, this.radiusScale, this.previousRadiusScale); + const fns = preparePieSeriesAnimationFunctions( + true, + this.properties.rotation, + this.radiusScale, + this.previousRadiusScale + ); fromToMotion(this.id, 'nodes', animationManager, [this.itemSelection, this.highlightSelection], fns.nodes); fromToMotion(this.id, `innerCircle`, animationManager, [this.innerCircleSelection], fns.innerCircle); @@ -1570,7 +1412,12 @@ export class PieSeries extends PolarSeries { this.ctx.animationManager.skipCurrentBatch(); } - const fns = preparePieSeriesAnimationFunctions(false, this.rotation, radiusScale, previousRadiusScale); + const fns = preparePieSeriesAnimationFunctions( + false, + this.properties.rotation, + radiusScale, + previousRadiusScale + ); fromToMotion( this.id, 'nodes', @@ -1593,7 +1440,12 @@ export class PieSeries extends PolarSeries { const { itemSelection, highlightSelection, radiusScale, previousRadiusScale } = this; const { animationManager } = this.ctx; - const fns = preparePieSeriesAnimationFunctions(false, this.rotation, radiusScale, previousRadiusScale); + const fns = preparePieSeriesAnimationFunctions( + false, + this.properties.rotation, + radiusScale, + previousRadiusScale + ); fromToMotion(this.id, 'nodes', animationManager, [itemSelection, highlightSelection], fns.nodes); fromToMotion(this.id, `innerCircle`, animationManager, [this.innerCircleSelection], fns.innerCircle); @@ -1605,10 +1457,10 @@ export class PieSeries extends PolarSeries { } getDatumIdFromData(datum: any) { - const { calloutLabelKey, sectorLabelKey, legendItemKey } = this; + const { calloutLabelKey, sectorLabelKey, legendItemKey } = this.properties; if (!this.processedData?.reduced?.animationValidation?.uniqueKeys) { - return undefined; + return; } if (legendItemKey) { diff --git a/packages/ag-charts-community/src/chart/series/polar/pieSeriesProperties.ts b/packages/ag-charts-community/src/chart/series/polar/pieSeriesProperties.ts new file mode 100644 index 0000000000..5df1872b1c --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/polar/pieSeriesProperties.ts @@ -0,0 +1,191 @@ +import type { + AgPieSeriesFormat, + AgPieSeriesFormatterParams, + AgPieSeriesLabelFormatterParams, + AgPieSeriesOptions, + AgPieSeriesTooltipRendererParams, +} from '../../../options/series/polar/pieOptions'; +import { DropShadow } from '../../../scene/dropShadow'; +import { BaseProperties, PropertiesArray } from '../../../util/properties'; +import { + BOOLEAN, + COLOR_STRING, + COLOR_STRING_ARRAY, + DEGREE, + FUNCTION, + LINE_DASH, + NUMBER, + OBJECT, + OBJECT_ARRAY, + POSITIVE_NUMBER, + RATIO, + STRING, + Validate, +} from '../../../util/validation'; +import { Caption } from '../../caption'; +import { Label } from '../../label'; +import { DEFAULT_FILLS, DEFAULT_STROKES } from '../../themes/defaultColors'; +import { SeriesProperties } from '../seriesProperties'; +import { SeriesTooltip } from '../seriesTooltip'; + +export class PieTitle extends Caption { + @Validate(BOOLEAN) + showInLegend = false; +} + +export class DoughnutInnerLabel extends Label { + @Validate(STRING) + text!: string; + + @Validate(NUMBER) + margin: number = 2; + + override set(properties: T, _reset?: boolean) { + return super.set(properties); + } +} + +export class DoughnutInnerCircle extends BaseProperties { + @Validate(COLOR_STRING) + fill: string = 'transparent'; + + @Validate(RATIO) + fillOpacity: number = 1; +} + +class PieSeriesCalloutLabel extends Label { + @Validate(POSITIVE_NUMBER) + offset = 3; // from the callout line + + @Validate(DEGREE) + minAngle = 0; + + @Validate(POSITIVE_NUMBER) + minSpacing = 4; + + @Validate(POSITIVE_NUMBER) + maxCollisionOffset = 50; + + @Validate(BOOLEAN) + avoidCollisions = true; +} + +class PieSeriesSectorLabel extends Label { + @Validate(NUMBER) + positionOffset = 0; + + @Validate(RATIO) + positionRatio = 0.5; +} + +class PieSeriesCalloutLine extends BaseProperties { + @Validate(COLOR_STRING_ARRAY, { optional: true }) + colors?: string[]; + + @Validate(POSITIVE_NUMBER) + length: number = 10; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; +} + +export class PieSeriesProperties extends SeriesProperties { + @Validate(STRING) + angleKey!: string; + + @Validate(STRING, { optional: true }) + angleName?: string; + + @Validate(STRING, { optional: true }) + radiusKey?: string; + + @Validate(STRING, { optional: true }) + radiusName?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + radiusMin?: number; + + @Validate(POSITIVE_NUMBER, { optional: true }) + radiusMax?: number; + + @Validate(STRING, { optional: true }) + calloutLabelKey?: string; + + @Validate(STRING, { optional: true }) + calloutLabelName?: string; + + @Validate(STRING, { optional: true }) + sectorLabelKey?: string; + + @Validate(STRING, { optional: true }) + sectorLabelName?: string; + + @Validate(STRING, { optional: true }) + legendItemKey?: string; + + @Validate(COLOR_STRING_ARRAY) + fills: string[] = Object.values(DEFAULT_FILLS); + + @Validate(COLOR_STRING_ARRAY) + strokes: string[] = Object.values(DEFAULT_STROKES); + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgPieSeriesFormatterParams) => AgPieSeriesFormat; + + @Validate(DEGREE) + rotation: number = 0; + + @Validate(NUMBER) + outerRadiusOffset: number = 0; + + @Validate(RATIO) + outerRadiusRatio: number = 1; + + @Validate(NUMBER) + innerRadiusOffset: number = 0; + + @Validate(RATIO) + innerRadiusRatio: number = 1; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(POSITIVE_NUMBER) + sectorSpacing: number = 1; + + @Validate(OBJECT_ARRAY) + readonly innerLabels = new PropertiesArray(DoughnutInnerLabel); + + @Validate(OBJECT) + readonly title = new PieTitle(); + + @Validate(OBJECT) + readonly innerCircle = new DoughnutInnerCircle(); + + @Validate(OBJECT) + readonly shadow = new DropShadow(); + + @Validate(OBJECT) + readonly calloutLabel = new PieSeriesCalloutLabel(); + + @Validate(OBJECT) + readonly sectorLabel = new PieSeriesSectorLabel(); + + @Validate(OBJECT) + readonly calloutLine = new PieSeriesCalloutLine(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-community/src/chart/series/series.ts b/packages/ag-charts-community/src/chart/series/series.ts index 904c64165e..3fe48ec5ff 100644 --- a/packages/ag-charts-community/src/chart/series/series.ts +++ b/packages/ag-charts-community/src/chart/series/series.ts @@ -3,7 +3,6 @@ import type { ModuleContextInitialiser } from '../../module/moduleMap'; import { ModuleMap } from '../../module/moduleMap'; import type { SeriesOptionInstance, SeriesOptionModule } from '../../module/optionModules'; import type { AgChartLabelFormatterParams, AgChartLabelOptions } from '../../options/chart/labelOptions'; -import type { InteractionRange } from '../../options/chart/types'; import type { AgSeriesMarkerFormatterParams, AgSeriesMarkerStyle, @@ -21,16 +20,6 @@ import { mergeDefaults } from '../../util/object'; import type { TypedEvent } from '../../util/observable'; import { Observable } from '../../util/observable'; import { ActionOnSet } from '../../util/proxy'; -import { - BOOLEAN, - COLOR_STRING, - INTERACTION_RANGE, - LINE_DASH, - POSITIVE_NUMBER, - RATIO, - STRING, - Validate, -} from '../../util/validation'; import { checkDatum } from '../../util/value'; import type { ChartAxis } from '../chartAxis'; import { ChartAxisDirection } from '../chartAxisDirection'; @@ -44,8 +33,8 @@ import type { ChartLegendDatum, ChartLegendType } from '../legendDatum'; import type { Marker } from '../marker/marker'; import type { BaseSeriesEvent, SeriesEventType } from './seriesEvents'; import type { SeriesGroupZIndexSubOrderType } from './seriesLayerManager'; +import type { SeriesProperties } from './seriesProperties'; import type { SeriesGrouping } from './seriesStateManager'; -import type { SeriesTooltip } from './seriesTooltip'; import type { ISeries, SeriesNodeDatum } from './seriesTypes'; /** Modes of matching user interactions to rendered nodes (e.g. hover or click) */ @@ -230,51 +219,6 @@ export class SeriesNodeClickEvent = { itemId: string; nodeData: S[]; @@ -297,12 +241,26 @@ export abstract class Series< extends Observable implements ISeries, ModuleContextInitialiser { + abstract readonly properties: SeriesProperties; + + pickModes: SeriesNodePickMode[]; + + @ActionOnSet>({ + changeValue: function (newVal, oldVal) { + this.onSeriesGroupingChange(oldVal, newVal); + }, + }) + seriesGrouping?: SeriesGrouping = undefined; + protected static readonly highlightedZIndex = 1000000000000; protected readonly NodeClickEvent: INodeClickEventConstructor = SeriesNodeClickEvent; - @Validate(STRING) - readonly id = createId(this); + protected readonly internalId = createId(this); + + get id() { + return this.properties?.id ?? this.internalId; + } readonly canHaveAxes: boolean; @@ -347,7 +305,7 @@ export abstract class Series< // Flag to determine if we should recalculate node data. protected nodeDataRefresh = true; - abstract tooltip: SeriesTooltip; + protected readonly moduleMap: SeriesModuleMap = new ModuleMap(this); protected _data?: any[]; protected _chartData?: any[]; @@ -361,6 +319,15 @@ export abstract class Series< return this._data ?? this._chartData; } + set visible(value: boolean) { + this.properties.visible = value; + this.visibleChanged(); + } + + get visible() { + return this.properties.visible; + } + protected onDataChange() { this.nodeDataRefresh = true; } @@ -377,34 +344,6 @@ export abstract class Series< return data && (!Array.isArray(data) || data.length > 0); } - @Validate(BOOLEAN) - protected _visible = true; - set visible(value: boolean) { - this._visible = value; - this.visibleChanged(); - } - get visible() { - return this._visible; - } - - @Validate(BOOLEAN) - showInLegend = true; - - pickModes: SeriesNodePickMode[]; - - @Validate(STRING) - cursor = 'default'; - - @Validate(INTERACTION_RANGE) - nodeClickRange: InteractionRange = 'exact'; - - @ActionOnSet>({ - changeValue: function (newVal, oldVal) { - this.onSeriesGroupingChange(oldVal, newVal); - }, - }) - seriesGrouping?: SeriesGrouping = undefined; - private onSeriesGroupingChange(prev?: SeriesGrouping, next?: SeriesGrouping) { const { id, type, visible, rootGroup, highlightGroup, annotationGroup } = this; @@ -450,9 +389,8 @@ export abstract class Series< }) { super(); - this.ctx = seriesOpts.moduleCtx; - const { + moduleCtx, useLabelLayer = false, pickModes = [SeriesNodePickMode.NEAREST_BY_MAIN_AXIS_FIRST], directionKeys = {}, @@ -461,13 +399,14 @@ export abstract class Series< canHaveAxes = false, } = seriesOpts; + this.ctx = moduleCtx; this.directionKeys = directionKeys; this.directionNames = directionNames; this.canHaveAxes = canHaveAxes; this.contentGroup = this.rootGroup.appendChild( new Group({ - name: `${this.id}-content`, + name: `${this.internalId}-content`, layer: !contentGroupVirtual, isVirtual: contentGroupVirtual, zIndex: Layers.SERIES_LAYER_ZINDEX, @@ -476,7 +415,7 @@ export abstract class Series< ); this.highlightGroup = new Group({ - name: `${this.id}-highlight`, + name: `${this.internalId}-highlight`, layer: !contentGroupVirtual, isVirtual: contentGroupVirtual, zIndex: Layers.SERIES_LAYER_ZINDEX, @@ -489,7 +428,7 @@ export abstract class Series< this.labelGroup = this.rootGroup.appendChild( new Group({ - name: `${this.id}-series-labels`, + name: `${this.internalId}-series-labels`, layer: useLabelLayer, zIndex: Layers.SERIES_LABEL_ZINDEX, }) @@ -569,7 +508,7 @@ export abstract class Series< } }; - addValues(...keys.map((key) => (this as any)[key])); + addValues(...keys.map((key) => (this.properties as any)[key])); return values; } @@ -617,7 +556,7 @@ export abstract class Series< public getOpacity(): number { const defaultOpacity = 1; - const { dimOpacity = 1, enabled = true } = this.highlightStyle.series; + const { dimOpacity = 1, enabled = true } = this.properties.highlightStyle.series; if (!enabled || dimOpacity === defaultOpacity) { return defaultOpacity; @@ -634,7 +573,7 @@ export abstract class Series< } protected getStrokeWidth(defaultStrokeWidth: number): number { - const { strokeWidth, enabled = true } = this.highlightStyle.series; + const { strokeWidth, enabled = true } = this.properties.highlightStyle.series; if (!enabled || strokeWidth === undefined) { // No change in styling for highlight cases. @@ -668,9 +607,7 @@ export abstract class Series< protected getModuleTooltipParams(): object { const params: object[] = this.moduleMap.values().map((mod) => mod.getTooltipParams()); - return params.reduce((total, current) => { - return { ...current, ...total }; - }, {}); + return params.reduce((total, current) => ({ ...current, ...total }), {}); } abstract getTooltipHtml(seriesDatum: any): string; @@ -756,10 +693,6 @@ export abstract class Series< return this.visible; } - readonly highlightStyle = new HighlightStyle(); - - protected readonly moduleMap: SeriesModuleMap = new ModuleMap(this); - getModuleMap(): SeriesModuleMap { return this.moduleMap; } diff --git a/packages/ag-charts-community/src/chart/series/seriesMarker.ts b/packages/ag-charts-community/src/chart/series/seriesMarker.ts index a8728101af..6015c2d563 100644 --- a/packages/ag-charts-community/src/chart/series/seriesMarker.ts +++ b/packages/ag-charts-community/src/chart/series/seriesMarker.ts @@ -3,7 +3,8 @@ import type { AgSeriesMarkerStyle, ISeriesMarker, } from '../../options/series/markerOptions'; -import { ChangeDetectable, RedrawType, SceneChangeDetection } from '../../scene/changeDetectable'; +import { RedrawType, SceneChangeDetection } from '../../scene/changeDetectable'; +import { BaseProperties } from '../../util/properties'; import type { RequireOptional } from '../../util/types'; import { BOOLEAN, @@ -25,7 +26,7 @@ const MARKER_SHAPE = predicateWithMessage( ); export class SeriesMarker - extends ChangeDetectable + extends BaseProperties implements ISeriesMarker> { @Validate(BOOLEAN) @@ -45,7 +46,7 @@ export class SeriesMarker @SceneChangeDetection({ redraw: RedrawType.MAJOR }) fill?: string; - @Validate(RATIO, { optional: true }) + @Validate(RATIO) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) fillOpacity: number = 1; @@ -53,11 +54,11 @@ export class SeriesMarker @SceneChangeDetection({ redraw: RedrawType.MAJOR }) stroke?: string; - @Validate(POSITIVE_NUMBER, { optional: true }) + @Validate(POSITIVE_NUMBER) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) strokeWidth: number = 1; - @Validate(RATIO, { optional: true }) + @Validate(RATIO) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) strokeOpacity: number = 1; diff --git a/packages/ag-charts-community/src/chart/series/seriesProperties.ts b/packages/ag-charts-community/src/chart/series/seriesProperties.ts new file mode 100644 index 0000000000..81f01db9f4 --- /dev/null +++ b/packages/ag-charts-community/src/chart/series/seriesProperties.ts @@ -0,0 +1,86 @@ +import type { InteractionRange } from '../../options/chart/types'; +import { BaseProperties } from '../../util/properties'; +import { + BOOLEAN, + COLOR_STRING, + INTERACTION_RANGE, + LINE_DASH, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, + Validate, +} from '../../util/validation'; +import type { SeriesTooltip } from './seriesTooltip'; + +export class SeriesItemHighlightStyle extends BaseProperties { + @Validate(COLOR_STRING, { optional: true }) + fill?: string = 'yellow'; + + @Validate(RATIO, { optional: true }) + fillOpacity?: number; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number; + + @Validate(RATIO, { optional: true }) + strokeOpacity?: number; + + @Validate(LINE_DASH, { optional: true }) + lineDash?: number[]; + + @Validate(POSITIVE_NUMBER, { optional: true }) + lineDashOffset?: number; +} + +class SeriesHighlightStyle extends BaseProperties { + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number; + + @Validate(RATIO, { optional: true }) + dimOpacity?: number; + + @Validate(BOOLEAN, { optional: true }) + enabled?: boolean; +} + +class TextHighlightStyle extends BaseProperties { + @Validate(COLOR_STRING, { optional: true }) + color?: string = 'black'; +} + +export class HighlightStyle extends BaseProperties { + @Validate(OBJECT) + readonly item = new SeriesItemHighlightStyle(); + + @Validate(OBJECT) + readonly series = new SeriesHighlightStyle(); + + @Validate(OBJECT) + readonly text = new TextHighlightStyle(); +} + +export abstract class SeriesProperties extends BaseProperties { + @Validate(STRING, { optional: true }) + id?: string; + + @Validate(BOOLEAN) + visible: boolean = true; + + @Validate(BOOLEAN) + showInLegend: boolean = true; + + @Validate(STRING) + cursor = 'default'; + + @Validate(INTERACTION_RANGE) + nodeClickRange: InteractionRange = 'exact'; + + @Validate(OBJECT) + readonly highlightStyle = new HighlightStyle(); + + abstract tooltip: SeriesTooltip; +} diff --git a/packages/ag-charts-community/src/chart/series/seriesTooltip.ts b/packages/ag-charts-community/src/chart/series/seriesTooltip.ts index 4ad5005b55..c0afbf417b 100644 --- a/packages/ag-charts-community/src/chart/series/seriesTooltip.ts +++ b/packages/ag-charts-community/src/chart/series/seriesTooltip.ts @@ -1,40 +1,34 @@ import type { AgSeriesTooltipRendererParams, AgTooltipRendererResult } from '../../options/chart/tooltipOptions'; -import { interpolate } from '../../util/string'; -import { BOOLEAN, FUNCTION, STRING, Validate } from '../../util/validation'; +import { BaseProperties } from '../../util/properties'; +import { BOOLEAN, FUNCTION, OBJECT, Validate } from '../../util/validation'; import { TooltipPosition, toTooltipHtml } from '../tooltip/tooltip'; type TooltipRenderer

= (params: P) => string | AgTooltipRendererResult; -type TooltipOverrides

= { format?: string; renderer?: TooltipRenderer

}; -class SeriesTooltipInteraction { +class SeriesTooltipInteraction extends BaseProperties { @Validate(BOOLEAN) - enabled = false; + enabled: boolean = false; } -export class SeriesTooltip

{ +export class SeriesTooltip

extends BaseProperties { @Validate(BOOLEAN) - enabled = true; + enabled: boolean = true; @Validate(BOOLEAN, { optional: true }) - showArrow?: boolean = undefined; - - @Validate(STRING, { optional: true }) - format?: string = undefined; + showArrow?: boolean; @Validate(FUNCTION, { optional: true }) - renderer?: TooltipRenderer

= undefined; + renderer?: TooltipRenderer

; + @Validate(OBJECT) readonly interaction = new SeriesTooltipInteraction(); + + @Validate(OBJECT) readonly position = new TooltipPosition(); - toTooltipHtml(defaults: AgTooltipRendererResult, params: P, overrides?: TooltipOverrides

) { - const formatFn = overrides?.format ?? this.format; - const rendererFn = overrides?.renderer ?? this.renderer; - if (formatFn) { - return toTooltipHtml({ content: interpolate(formatFn, params) }, defaults); - } - if (rendererFn) { - return toTooltipHtml(rendererFn(params), defaults); + toTooltipHtml(defaults: AgTooltipRendererResult, params: P) { + if (this.renderer) { + return toTooltipHtml(this.renderer(params), defaults); } return toTooltipHtml(defaults); } diff --git a/packages/ag-charts-community/src/chart/series/seriesTypes.ts b/packages/ag-charts-community/src/chart/series/seriesTypes.ts index d11938ee4e..8fb39b751f 100644 --- a/packages/ag-charts-community/src/chart/series/seriesTypes.ts +++ b/packages/ag-charts-community/src/chart/series/seriesTypes.ts @@ -1,19 +1,16 @@ -import type { InteractionRange } from '../../options/chart/types'; import type { BBox } from '../../scene/bbox'; import type { Group } from '../../scene/group'; import type { Point, SizedPoint } from '../../scene/point'; import type { ChartAxis } from '../chartAxis'; import type { ChartAxisDirection } from '../chartAxisDirection'; import type { ChartLegendDatum, ChartLegendType } from '../legendDatum'; -import type { SeriesTooltip } from './seriesTooltip'; +import type { SeriesProperties } from './seriesProperties'; export interface ISeries { id: string; axes: Record; - cursor: string; contentGroup: Group; - tooltip: SeriesTooltip; - nodeClickRange: InteractionRange; + properties: SeriesProperties; hasEventListener(type: string): boolean; update(opts: { seriesRect?: BBox }): Promise; fireNodeClickEvent(event: Event, datum: TDatum): void; diff --git a/packages/ag-charts-community/src/chart/test/utils.ts b/packages/ag-charts-community/src/chart/test/utils.ts index 2ae5df8898..6fee544784 100644 --- a/packages/ag-charts-community/src/chart/test/utils.ts +++ b/packages/ag-charts-community/src/chart/test/utils.ts @@ -45,6 +45,10 @@ export const CANVAS_TO_BUFFER_DEFAULTS: PngConfig = { compressionLevel: 6, filte const CANVAS_WIDTH = 800; const CANVAS_HEIGHT = 600; +async function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + export function prepareTestOptions(options: T, container = document.body) { options.autoSize = false; options.width = CANVAS_WIDTH; @@ -218,9 +222,7 @@ export function hoverAction(x: number, y: number): (chart: Chart | AgChartProxy) target?.dispatchEvent(mouseMoveEvent({ offsetX: x - 1, offsetY: y - 1 })); target?.dispatchEvent(mouseMoveEvent({ offsetX: x, offsetY: y })); - return new Promise((resolve) => { - setTimeout(resolve, 50); - }); + return delay(50); }; } @@ -231,9 +233,7 @@ export function clickAction(x: number, y: number): (chart: Chart | AgChartProxy) checkTargetValid(target); target?.dispatchEvent(clickEvent({ offsetX: x, offsetY: y })); - return new Promise((resolve) => { - setTimeout(resolve, 50); - }); + return delay(50); }; } @@ -241,17 +241,13 @@ export function doubleClickAction(x: number, y: number): (chart: Chart | AgChart return async (chartOrProxy) => { const chart = deproxy(chartOrProxy); const target = chart.scene.canvas.element; - // A double click is always preceeded by two single clicks, simulate here to ensure correct handling + // A double click is always preceded by two single clicks, simulate here to ensure correct handling target?.dispatchEvent(clickEvent({ offsetX: x, offsetY: y })); target?.dispatchEvent(clickEvent({ offsetX: x, offsetY: y })); - await new Promise((resolve) => { - setTimeout(resolve, 50); - }); + await delay(50); await waitForChartStability(chart); target?.dispatchEvent(doubleClickEvent({ offsetX: x, offsetY: y })); - return new Promise((resolve) => { - setTimeout(resolve, 50); - }); + return delay(50); }; } @@ -260,9 +256,7 @@ export function scrollAction(x: number, y: number, delta: number): (chart: Chart const chart = deproxy(chartOrProxy); const target = chart.scene.canvas.element; target?.dispatchEvent(wheelEvent({ clientX: x, clientY: y, deltaY: delta })); - await new Promise((resolve) => { - setTimeout(resolve, 50); - }); + await delay(50); }; } diff --git a/packages/ag-charts-community/src/chart/themes/chartTheme.test.ts b/packages/ag-charts-community/src/chart/themes/chartTheme.test.ts index 7afb43b521..05ed424fe2 100644 --- a/packages/ag-charts-community/src/chart/themes/chartTheme.test.ts +++ b/packages/ag-charts-community/src/chart/themes/chartTheme.test.ts @@ -198,22 +198,22 @@ describe('ChartTheme', () => { const strokes = ['cyan', 'cyan', 'cyan', 'cyan', 'cyan']; for (let i = 0; i < 5; i++) { expect(chart.series[i].type).toBe('bar'); - expect((chart.series[i] as BarSeries).fill).toEqual(fills[i]); - expect((chart.series[i] as BarSeries).stroke).toEqual(strokes[i]); - expect((chart.series[i] as BarSeries).label.enabled).toBe(true); - expect((chart.series[i] as BarSeries).label.color).toBe('yellow'); - expect((chart.series[i] as BarSeries).label.fontSize).toBe(18); - expect((chart.series[i] as BarSeries).tooltip.enabled).toBe(false); - expect((chart.series[i] as BarSeries).tooltip.renderer).toBe(tooltipRenderer); + expect((chart.series[i] as BarSeries).properties.fill).toEqual(fills[i]); + expect((chart.series[i] as BarSeries).properties.stroke).toEqual(strokes[i]); + expect((chart.series[i] as BarSeries).properties.label.enabled).toBe(true); + expect((chart.series[i] as BarSeries).properties.label.color).toBe('yellow'); + expect((chart.series[i] as BarSeries).properties.label.fontSize).toBe(18); + expect((chart.series[i] as BarSeries).properties.tooltip.enabled).toBe(false); + expect((chart.series[i] as BarSeries).properties.tooltip.renderer).toBe(tooltipRenderer); } const areaFills = ['blue', 'red', 'green', 'blue', 'red']; const areaStrokes = ['cyan', 'cyan', 'cyan', 'cyan', 'cyan']; for (let i = 5; i < 10; i++) { expect(chart.series[i].type).toBe('area'); - expect((chart.series[i] as unknown as AreaSeries).fill).toEqual(areaFills[i - 5]); - expect((chart.series[i] as unknown as AreaSeries).stroke).toEqual(areaStrokes[i - 5]); - expect((chart.series[i] as unknown as AreaSeries).marker.formatter).toBe(markerFormatter); + expect((chart.series[i] as unknown as AreaSeries).properties.fill).toEqual(areaFills[i - 5]); + expect((chart.series[i] as unknown as AreaSeries).properties.stroke).toEqual(areaStrokes[i - 5]); + expect((chart.series[i] as unknown as AreaSeries).properties.marker.formatter).toBe(markerFormatter); } }); }); @@ -290,13 +290,13 @@ describe('ChartTheme', () => { expect((chart as any).background.fill).toBe('red'); expect(chart.series[0].type).toBe('pie'); - expect((chart.series[0] as PieSeries).fills).toEqual(['red', 'green', 'blue']); - expect((chart.series[0] as PieSeries).strokes).toEqual(['cyan', 'cyan', 'cyan']); - expect((chart.series[0] as PieSeries).calloutLabel.enabled).toBe(true); - expect((chart.series[0] as PieSeries).calloutLabel.color).toBe('yellow'); - expect((chart.series[0] as PieSeries).calloutLabel.fontSize).toBe(18); - expect((chart.series[0] as PieSeries).tooltip.enabled).toBe(false); - expect((chart.series[0] as PieSeries).tooltip.renderer).toBe(tooltipRenderer); + expect((chart.series[0] as PieSeries).properties.fills).toEqual(['red', 'green', 'blue']); + expect((chart.series[0] as PieSeries).properties.strokes).toEqual(['cyan', 'cyan', 'cyan']); + expect((chart.series[0] as PieSeries).properties.calloutLabel.enabled).toBe(true); + expect((chart.series[0] as PieSeries).properties.calloutLabel.color).toBe('yellow'); + expect((chart.series[0] as PieSeries).properties.calloutLabel.fontSize).toBe(18); + expect((chart.series[0] as PieSeries).properties.tooltip.enabled).toBe(false); + expect((chart.series[0] as PieSeries).properties.tooltip.renderer).toBe(tooltipRenderer); }); }); @@ -442,13 +442,13 @@ describe('ChartTheme', () => { const strokes = ['cyan', 'cyan', 'cyan', 'cyan', 'cyan']; for (let i = 0; i < 5; i++) { expect(chart.series[i].type).toBe('bar'); - expect((chart.series[i] as BarSeries).fill).toEqual(fills[i]); - expect((chart.series[i] as BarSeries).stroke).toEqual(strokes[i]); - expect((chart.series[i] as BarSeries).label.enabled).toBe(true); - expect((chart.series[i] as BarSeries).label.color).toBe('blue'); - expect((chart.series[i] as BarSeries).label.fontSize).toBe(18); - expect((chart.series[i] as BarSeries).tooltip.enabled).toBe(false); - expect((chart.series[i] as BarSeries).tooltip.renderer).toBe(columnTooltipRenderer); + expect((chart.series[i] as BarSeries).properties.fill).toEqual(fills[i]); + expect((chart.series[i] as BarSeries).properties.stroke).toEqual(strokes[i]); + expect((chart.series[i] as BarSeries).properties.label.enabled).toBe(true); + expect((chart.series[i] as BarSeries).properties.label.color).toBe('blue'); + expect((chart.series[i] as BarSeries).properties.label.fontSize).toBe(18); + expect((chart.series[i] as BarSeries).properties.tooltip.enabled).toBe(false); + expect((chart.series[i] as BarSeries).properties.tooltip.renderer).toBe(columnTooltipRenderer); } }); @@ -465,13 +465,13 @@ describe('ChartTheme', () => { expect((chart as any).background.fill).toBe('red'); expect(chart.series[0].type).toBe('pie'); - expect((chart.series[0] as PieSeries).fills).toEqual(['red', 'green', 'blue']); - expect((chart.series[0] as PieSeries).strokes).toEqual(['cyan', 'cyan', 'cyan']); - expect((chart.series[0] as PieSeries).calloutLabel.enabled).toBe(true); - expect((chart.series[0] as PieSeries).calloutLabel.color).toBe('yellow'); - expect((chart.series[0] as PieSeries).calloutLabel.fontSize).toBe(18); - expect((chart.series[0] as PieSeries).tooltip.enabled).toBe(false); - expect((chart.series[0] as PieSeries).tooltip.renderer).toBe(pieTooltipRenderer); + expect((chart.series[0] as PieSeries).properties.fills).toEqual(['red', 'green', 'blue']); + expect((chart.series[0] as PieSeries).properties.strokes).toEqual(['cyan', 'cyan', 'cyan']); + expect((chart.series[0] as PieSeries).properties.calloutLabel.enabled).toBe(true); + expect((chart.series[0] as PieSeries).properties.calloutLabel.color).toBe('yellow'); + expect((chart.series[0] as PieSeries).properties.calloutLabel.fontSize).toBe(18); + expect((chart.series[0] as PieSeries).properties.tooltip.enabled).toBe(false); + expect((chart.series[0] as PieSeries).properties.tooltip.renderer).toBe(pieTooltipRenderer); }); }); @@ -860,10 +860,10 @@ describe('ChartTheme', () => { expect(series[1].type).toEqual('bar'); expect(series[2].type).toEqual('line'); expect(series[3].type).toEqual('area'); - expect((series[0] as BarSeries).strokeWidth).toEqual(16); - expect((series[1] as BarSeries).strokeWidth).toEqual(16); - expect((series[2] as LineSeries).strokeWidth).toEqual(17); - expect((series[3] as unknown as AreaSeries).strokeWidth).toEqual(18); + expect((series[0] as BarSeries).properties.strokeWidth).toEqual(16); + expect((series[1] as BarSeries).properties.strokeWidth).toEqual(16); + expect((series[2] as LineSeries).properties.strokeWidth).toEqual(17); + expect((series[3] as unknown as AreaSeries).properties.strokeWidth).toEqual(18); }); }); }); diff --git a/packages/ag-charts-community/src/chart/tooltip/tooltip.ts b/packages/ag-charts-community/src/chart/tooltip/tooltip.ts index b52f85b022..486b5d0b7c 100644 --- a/packages/ag-charts-community/src/chart/tooltip/tooltip.ts +++ b/packages/ag-charts-community/src/chart/tooltip/tooltip.ts @@ -1,6 +1,7 @@ import type { AgTooltipRendererResult, InteractionRange, TextWrap } from '../../options/agChartOptions'; import { BBox } from '../../scene/bbox'; import { injectStyle } from '../../util/dom'; +import { BaseProperties } from '../../util/properties'; import { BOOLEAN, INTERACTION_RANGE, @@ -187,7 +188,7 @@ export function toTooltipHtml(input: string | AgTooltipRendererResult, defaults? type TooltipPositionType = 'pointer' | 'node'; -export class TooltipPosition { +export class TooltipPosition extends BaseProperties { @Validate(UNION(['pointer', 'node'], 'a position type')) /** The type of positioning for the tooltip. By default, the tooltip follows the pointer. */ type: TooltipPositionType = 'pointer'; diff --git a/packages/ag-charts-community/src/module-support.ts b/packages/ag-charts-community/src/module-support.ts index 45c86ad8e9..775208f197 100644 --- a/packages/ag-charts-community/src/module-support.ts +++ b/packages/ag-charts-community/src/module-support.ts @@ -6,9 +6,11 @@ export * from './util/dom'; export * from './util/deprecation'; export * from './util/number'; export * from './util/object'; +export * from './util/properties'; export * from './util/proxy'; export * from './util/shapes'; export * from './util/types'; +export * from './util/type-guards'; export * from './util/theme'; export * from './module/baseModule'; export * from './module/coreModules'; @@ -38,6 +40,7 @@ export * from './chart/layers'; export * from './chart/series/series'; export * from './chart/series/seriesEvents'; export * from './chart/series/seriesLabelUtil'; +export * from './chart/series/seriesProperties'; export * from './chart/series/seriesMarker'; export * from './chart/series/seriesTooltip'; export * from './chart/series/seriesTypes'; @@ -51,6 +54,7 @@ export * from './chart/series/cartesian/labelUtil'; export * from './chart/series/cartesian/pathUtil'; export * from './chart/series/polar/polarSeries'; export * from './chart/series/hierarchy/hierarchySeries'; +export * from './chart/series/hierarchy/hierarchySeriesProperties'; export * from './chart/axis/axis'; export * from './chart/axis/axisLabel'; export * from './chart/axis/axisTick'; diff --git a/packages/ag-charts-community/src/options/chart/types.ts b/packages/ag-charts-community/src/options/chart/types.ts index ea0cbf1585..1ff1957e9e 100644 --- a/packages/ag-charts-community/src/options/chart/types.ts +++ b/packages/ag-charts-community/src/options/chart/types.ts @@ -12,7 +12,9 @@ export type FontWeight = | '600' | '700' | '800' - | '900'; + | '900' + | '1000' + | number; export type FontFamily = string; export type FontSize = number; diff --git a/packages/ag-charts-community/src/options/series/cartesian/bubbleOptions.ts b/packages/ag-charts-community/src/options/series/cartesian/bubbleOptions.ts index 341bc727de..f258bd30d5 100644 --- a/packages/ag-charts-community/src/options/series/cartesian/bubbleOptions.ts +++ b/packages/ag-charts-community/src/options/series/cartesian/bubbleOptions.ts @@ -18,12 +18,12 @@ export interface AgBubbleSeriesMarker extends AgSeriesMarkerOptions extends AgBaseSeriesThemeableOptions { + /** The title to use for the series. Defaults to `yName` if it exists, or `yKey` if not. */ + title?: string; /** Configuration for the markers used in the series. */ marker?: AgBubbleSeriesMarker; /** Configuration for the labels shown on top of data points. */ label?: AgChartLabelOptions; - /** The title to use for the series. Defaults to `yName` if it exists, or `yKey` if not. */ - title?: string; /** Series-specific tooltip configuration. */ tooltip?: AgSeriesTooltip>; } diff --git a/packages/ag-charts-community/src/options/series/cartesian/histogramOptions.ts b/packages/ag-charts-community/src/options/series/cartesian/histogramOptions.ts index ba0ba2e8ef..b9d1935955 100644 --- a/packages/ag-charts-community/src/options/series/cartesian/histogramOptions.ts +++ b/packages/ag-charts-community/src/options/series/cartesian/histogramOptions.ts @@ -6,7 +6,10 @@ import type { AgCartesianSeriesTooltipRendererParams } from './cartesianSeriesTo import type { FillOptions, LineDashOptions, StrokeOptions } from './commonOptions'; export interface AgHistogramSeriesTooltipRendererParams - extends AgCartesianSeriesTooltipRendererParams> {} + extends Omit>, 'yKey'> { + /** yKey as specified on series options. */ + readonly yKey?: string; +} export type AgHistogramSeriesLabelFormatterParams = AgHistogramSeriesOptionsKeys & AgHistogramSeriesOptionsNames; diff --git a/packages/ag-charts-community/src/options/series/markerOptions.ts b/packages/ag-charts-community/src/options/series/markerOptions.ts index 6bbb1bf20d..6349f77ec9 100644 --- a/packages/ag-charts-community/src/options/series/markerOptions.ts +++ b/packages/ag-charts-community/src/options/series/markerOptions.ts @@ -2,15 +2,6 @@ import type { AgChartCallbackParams } from '../chart/callbackOptions'; import type { MarkerShape, PixelSize } from '../chart/types'; import type { FillOptions, StrokeOptions } from './cartesian/commonOptions'; -export interface AgSeriesMarkerOptions extends AgSeriesMarkerStyle { - /** Whether to show markers. */ - enabled?: boolean; - /** The shape to use for the markers. You can also supply a custom marker by providing a `Marker` subclass. */ - shape?: MarkerShape; - /** Function used to return formatting for individual markers, based on the supplied information. If the current marker is highlighted, the `highlighted` property will be set to `true`; make sure to check this if you want to differentiate between the highlighted and un-highlighted states. */ - formatter?: (params: AgSeriesMarkerFormatterParams & TParams) => AgSeriesMarkerStyle | undefined; -} - export interface AgSeriesMarkerStyle extends FillOptions, StrokeOptions { /** The size in pixels of the markers. */ size?: PixelSize; @@ -20,6 +11,15 @@ export interface AgSeriesMarkerFormatterParams extends AgChartCallbackPa highlighted: boolean; } +export interface AgSeriesMarkerOptions extends AgSeriesMarkerStyle { + /** Whether to show markers. */ + enabled?: boolean; + /** The shape to use for the markers. You can also supply a custom marker by providing a `Marker` subclass. */ + shape?: MarkerShape; + /** Function used to return formatting for individual markers, based on the supplied information. If the current marker is highlighted, the `highlighted` property will be set to `true`; make sure to check this if you want to differentiate between the highlighted and un-highlighted states. */ + formatter?: (params: AgSeriesMarkerFormatterParams & TParams) => AgSeriesMarkerStyle | undefined; +} + export interface ISeriesMarker extends AgSeriesMarkerOptions { getStyle: () => AgSeriesMarkerStyle; } diff --git a/packages/ag-charts-community/src/scene/dropShadow.ts b/packages/ag-charts-community/src/scene/dropShadow.ts index 0387190369..8129e3a649 100644 --- a/packages/ag-charts-community/src/scene/dropShadow.ts +++ b/packages/ag-charts-community/src/scene/dropShadow.ts @@ -1,25 +1,26 @@ +import { BaseProperties } from '../util/properties'; import { BOOLEAN, COLOR_STRING, NUMBER, POSITIVE_NUMBER, Validate } from '../util/validation'; -import { ChangeDetectable, RedrawType } from './changeDetectable'; +import { RedrawType } from './changeDetectable'; import { SceneChangeDetection } from './node'; -export class DropShadow extends ChangeDetectable { +export class DropShadow extends BaseProperties { @Validate(BOOLEAN) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - enabled = true; + enabled: boolean = true; @Validate(COLOR_STRING) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - color = 'rgba(0, 0, 0, 0.5)'; + color: string = 'rgba(0, 0, 0, 0.5)'; @Validate(NUMBER) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - xOffset = 0; + xOffset: number = 0; @Validate(NUMBER) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - yOffset = 0; + yOffset: number = 0; @Validate(POSITIVE_NUMBER) @SceneChangeDetection({ redraw: RedrawType.MAJOR }) - blur = 5; + blur: number = 5; } diff --git a/packages/ag-charts-community/src/scene/shape/rect.test.ts b/packages/ag-charts-community/src/scene/shape/rect.test.ts index 864ae6da79..3f063e2896 100644 --- a/packages/ag-charts-community/src/scene/shape/rect.test.ts +++ b/packages/ag-charts-community/src/scene/shape/rect.test.ts @@ -9,7 +9,7 @@ describe('Rect', () => { describe('rendering', () => { const canvasCtx = setupMockCanvas({ height: 1000 }); - const shadowFn = (offset: number) => Object.assign(new DropShadow(), { xOffset: offset, yOffset: offset }); + const shadowFn = (offset: number) => new DropShadow().set({ xOffset: offset, yOffset: offset }); const GAP = 20; const DEFAULTS: Partial = { width: 20, height: 20 }; @@ -191,7 +191,13 @@ describe('Rect', () => { // Render. ctx.save(); - rect.render({ ctx, forceRender: true, resized: false, debugNodes: {} }); + rect.render({ + ctx, + forceRender: true, + resized: false, + debugNodes: {}, + devicePixelRatio: 1, + }); ctx.restore(); // Prepare for next case. diff --git a/packages/ag-charts-community/src/sparklines-util.ts b/packages/ag-charts-community/src/sparklines-util.ts index a8acc9e175..19246cbf41 100644 --- a/packages/ag-charts-community/src/sparklines-util.ts +++ b/packages/ag-charts-community/src/sparklines-util.ts @@ -9,7 +9,6 @@ export * from './util/number'; export { extent, normalisedExtent, normalisedExtentWithMetadata } from './util/array'; export { toFixed, isEqual as isNumberEqual } from './util/number'; export { tickFormat } from './util/numberFormat'; -export { interpolate as interpolateString } from './util/string'; export * from './util/sanitize'; export { default as ticks, tickStep, range } from './util/ticks'; diff --git a/packages/ag-charts-community/src/util/json.ts b/packages/ag-charts-community/src/util/json.ts index e4c006c408..75bda6a33f 100644 --- a/packages/ag-charts-community/src/util/json.ts +++ b/packages/ag-charts-community/src/util/json.ts @@ -1,4 +1,5 @@ import { Logger } from './logger'; +import { isProperties } from './properties'; import type { DeepPartial } from './types'; const CLASS_INSTANCE_TYPE = 'class-instance'; @@ -248,6 +249,10 @@ export function jsonApply jsonApply(new ctr(), v, { @@ -312,16 +319,25 @@ export function jsonApply extends ChangeDetectable { - constructor(properties?: T) { - super(); - if (properties) { - this.set(properties); - } - } - - set(properties: T) { + set(properties: T, reset?: boolean) { for (const propertyKey of listDecoratedProperties(this)) { - if (Object.hasOwn(properties, propertyKey)) { + if (reset || Object.hasOwn(properties, propertyKey)) { const value = properties[propertyKey as keyof T]; const self = this as any; if (isProperties(self[propertyKey])) { - self[propertyKey].set(value); + // re-set property to force re-validation + self[propertyKey] = self[propertyKey].set(value, reset); } else { self[propertyKey] = value; } } } + return this; } isValid>(this: TContext) { @@ -29,8 +25,33 @@ export class BaseProperties extends ChangeDetectable return optional || typeof this[propertyKey as keyof TContext] !== 'undefined'; }); } + + toJson(this: T) { + return listDecoratedProperties(this).reduce>((object, propertyKey) => { + object[propertyKey] = this[propertyKey as keyof T]; + return object; + }, {}); + } +} + +export class PropertiesArray extends Array { + private itemFactory!: new () => T; + + constructor(itemFactory: new () => T, arrayLength = 0) { + super(arrayLength); + Object.defineProperty(this, 'itemFactory', { value: itemFactory, enumerable: false, configurable: false }); + } + + set(properties: object[]): PropertiesArray { + if (isArray(properties)) { + const newArray = new PropertiesArray(this.itemFactory); + newArray.push(...properties.map((property) => new this.itemFactory().set(property))); + return newArray; + } + return this; + } } export function isProperties(value: unknown): value is BaseProperties { - return value instanceof BaseProperties; + return value instanceof BaseProperties || value instanceof PropertiesArray; } diff --git a/packages/ag-charts-community/src/util/string.test.ts b/packages/ag-charts-community/src/util/string.test.ts deleted file mode 100644 index 3f343858f4..0000000000 --- a/packages/ag-charts-community/src/util/string.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; - -import { interpolate } from './string'; -import { buildFormatter } from './timeFormat'; - -describe('interpolate', () => { - it('should substitute #{key} with values from the given object', () => { - const name = 'first name'; - const result = interpolate(`My ${name} is #{name} and I live in #{place}`, { - name: 'Vitaly', - place: 'London', - }); - expect(result).toBe('My first name is Vitaly and I live in London'); - }); - - it('should strip #{key} if the key is not in the given object', () => { - const result = interpolate('#{something} #{this} #{stuff} should be gone', { - something: 'Ebola', - }); - expect(result).toBe('Ebola should be gone'); - }); - - it('should convert numbers and objects with toString method to strings', () => { - const result = interpolate('#{caption}: #{value}', { - caption: { - first: 'My', - second: 'favorite', - third: 'number', - toString: function () { - return this.first + ' ' + this.second + ' ' + this.third; - }, - }, - value: 42, - }); - expect(result).toBe('My favorite number: 42'); - }); - - it('should format numbers (using Intl.NumberFormat) and dates', () => { - const format = '%A, %b %d %Y'; - const formatter = buildFormatter(format); - const amount1 = 42000000; - const amount2 = 1234; - const locales = 'en-GB'; - const date = new Date('Wed Sep 23 2020'); - const options = { - style: 'unit', - unit: 'liter', - unitDisplay: 'long', - }; - const formattedAmount1 = amount1.toLocaleString(locales, options); - const formattedAmount2 = amount2.toLocaleString(locales, options); - const formattedDate = formatter(date); - const result1 = interpolate( - 'I drank #{amount1:liters} of beer and #{amount2:liters} of vodka on #{day:date}', - { - amount1, - amount2, - day: date, - }, - { - liters: { - locales, - options, - }, - date: '%A, %b %d %Y', - } - ); - expect(result1).toBe( - `I drank ${formattedAmount1} of beer and ${formattedAmount2} of vodka on ${formattedDate}` - ); - }); -}); diff --git a/packages/ag-charts-community/src/util/string.ts b/packages/ag-charts-community/src/util/string.ts deleted file mode 100644 index f724eea8d7..0000000000 --- a/packages/ag-charts-community/src/util/string.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { buildFormatter } from './timeFormat'; - -const interpolatePattern = /(#\{(.*?)\})/g; - -type NumberFormat = { - locales?: string | string[]; - options?: any; -}; - -type DateFormat = string; - -type ValueFormat = NumberFormat | DateFormat; - -export function interpolate( - input: string, - values: { [key in string]: any }, - formats?: { [key in string]: ValueFormat } -): string { - return input.replace(interpolatePattern, function (...args) { - const name = args[2]; - const [valueName, formatName] = name.split(':'); - const value = values[valueName]; - - if (typeof value === 'number') { - const format = formatName && formats && formats[formatName]; - - if (format) { - const { locales, options } = format as NumberFormat; - return value.toLocaleString(locales, options); - } - - return String(value); - } - - if (value instanceof Date) { - const format = formatName && formats && formats[formatName]; - if (typeof format === 'string') { - const formatter = buildFormatter(format); - return formatter(value); - } - return value.toDateString(); - } - - if (typeof value === 'string' || value?.toString) { - return String(value); - } - - return ''; - }); -} diff --git a/packages/ag-charts-community/src/util/validation.ts b/packages/ag-charts-community/src/util/validation.ts index 0e17864c1e..778d8eb3d6 100644 --- a/packages/ag-charts-community/src/util/validation.ts +++ b/packages/ag-charts-community/src/util/validation.ts @@ -35,14 +35,19 @@ export interface ValidateNumberPredicate extends ValidatePredicate { restrict(options: { min?: number; max?: number }): ValidatePredicate; } +export interface ValidateObjectPredicate extends ValidatePredicate { + restrict(objectType: Function): ValidatePredicate; +} + export function Validate(predicate: ValidatePredicate, options: ValidateOptions = {}) { const { optional = false } = options; return addTransformToInstanceProperty( (target, property, value: any) => { const context = { ...options, target, property }; if ((optional && typeof value === 'undefined') || predicate(value, context)) { - if (isProperties(target[property])) { - target[property].set(value); + if (isProperties(target[property]) && !isProperties(value)) { + // properties array set can return a new instance + target[property] = target[property].set(value); return target[property]; } return value; @@ -74,22 +79,22 @@ export function Validate(predicate: ValidatePredicate, options: ValidateOptions ); } -export const AND = (...predicates: ValidatePredicate[]) => { - return predicateWithMessage( +export const AND = (...predicates: ValidatePredicate[]) => + predicateWithMessage( (value, ctx) => predicates.every((predicate) => predicate(value, ctx)), (ctx) => predicates.map(getPredicateMessageMapper(ctx)).filter(Boolean).join(' AND ') ); -}; -export const OR = (...predicates: ValidatePredicate[]) => { - return predicateWithMessage( +export const OR = (...predicates: ValidatePredicate[]) => + predicateWithMessage( (value, ctx) => predicates.some((predicate) => predicate(value, ctx)), (ctx) => predicates.map(getPredicateMessageMapper(ctx)).filter(Boolean).join(' OR ') ); -}; -export const OBJECT = predicateWithMessage( - (value, ctx) => isProperties(value) || (isObject(value) && isProperties(ctx.target[ctx.property])), - 'an object' +export const OBJECT = attachObjectRestrictions( + predicateWithMessage( + (value, ctx) => isProperties(value) || (isObject(value) && isProperties(ctx.target[ctx.property])), + 'an object' + ) ); export const BOOLEAN = predicateWithMessage(isBoolean, 'a boolean'); export const FUNCTION = predicateWithMessage(isFunction, 'a function'); @@ -139,6 +144,7 @@ export const BOOLEAN_ARRAY = ARRAY_OF(BOOLEAN, 'boolean values'); export const NUMBER_ARRAY = ARRAY_OF(NUMBER, 'numbers'); export const STRING_ARRAY = ARRAY_OF(STRING, 'strings'); export const DATE_ARRAY = predicateWithMessage(ARRAY_OF(DATE), 'Date objects'); +export const OBJECT_ARRAY = predicateWithMessage(ARRAY_OF(OBJECT), 'objects'); export const LINE_CAP = UNION(['butt', 'round', 'square'], 'a line cap'); export const LINE_JOIN = UNION(['round', 'bevel', 'miter'], 'a line join'); @@ -232,6 +238,18 @@ function attachNumberRestrictions(predicate: ValidatePredicate): ValidateNumberP }); } +function attachObjectRestrictions(predicate: ValidatePredicate): ValidateObjectPredicate { + return Object.assign(predicate, { + restrict(objectType: Function) { + const isInstanceOf = (value: unknown) => isProperties(value) && value instanceof objectType; + return predicateWithMessage( + (value, ctx) => isInstanceOf(value) || (isObject(value) && isInstanceOf(ctx.target[ctx.property])), + (ctx) => getPredicateMessage(predicate, ctx) ?? 'an object' + ); + }, + }); +} + function stringify(value: any): string { if (typeof value === 'number') { if (isNaN(value)) return 'NaN'; diff --git a/packages/ag-charts-enterprise/src/features/error-bar/errorBar.ts b/packages/ag-charts-enterprise/src/features/error-bar/errorBar.ts index bea01a1a58..13a44dd6c9 100644 --- a/packages/ag-charts-enterprise/src/features/error-bar/errorBar.ts +++ b/packages/ag-charts-enterprise/src/features/error-bar/errorBar.ts @@ -1,29 +1,11 @@ -import type { AgErrorBarOptions, AgErrorBarThemeableOptions, _Scale } from 'ag-charts-community'; +import type { AgErrorBarThemeableOptions, _Scale } from 'ag-charts-community'; import { AgErrorBarSupportedSeriesTypes, _ModuleSupport, _Scene } from 'ag-charts-community'; -import type { - ErrorBarCapFormatter, - ErrorBarFormatter, - ErrorBarNodeDatum, - ErrorBarStylingOptions, -} from './errorBarNode'; +import type { ErrorBarNodeDatum, ErrorBarStylingOptions } from './errorBarNode'; import { ErrorBarGroup, ErrorBarNode } from './errorBarNode'; +import { ErrorBarProperties } from './errorBarProperties'; -const { - fixNumericExtent, - mergeDefaults, - valueProperty, - ChartAxisDirection, - Validate, - BOOLEAN, - COLOR_STRING, - FUNCTION, - LINE_DASH, - NUMBER, - POSITIVE_NUMBER, - STRING, - RATIO, -} = _ModuleSupport; +const { isDefined, fixNumericExtent, mergeDefaults, valueProperty, ChartAxisDirection } = _ModuleSupport; type ErrorBoundCartesianSeries = Omit< _ModuleSupport.CartesianSeries<_Scene.Node, ErrorBarNodeDatum>, @@ -53,109 +35,33 @@ type SeriesDataProcessedEvent = _ModuleSupport.SeriesDataProcessedEvent; type SeriesDataUpdateEvent = _ModuleSupport.SeriesDataUpdateEvent; type SeriesVisibilityEvent = _ModuleSupport.SeriesVisibilityEvent; -class ErrorBarCap implements NonNullable { - @Validate(BOOLEAN, { optional: true }) - visible?: boolean = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth?: number = undefined; - - @Validate(RATIO, { optional: true }) - strokeOpacity?: number = undefined; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[]; - - @Validate(POSITIVE_NUMBER, { optional: true }) - lineDashOffset?: number; - - @Validate(NUMBER, { optional: true }) - length?: number = undefined; - - @Validate(RATIO, { optional: true }) - lengthRatio?: number = undefined; - - @Validate(FUNCTION, { optional: true }) - formatter?: ErrorBarCapFormatter = undefined; -} - -export class ErrorBars - extends _ModuleSupport.BaseModuleInstance - implements _ModuleSupport.ModuleInstance, _ModuleSupport.SeriesOptionInstance, AgErrorBarOptions -{ - @Validate(STRING, { optional: true }) - yLowerKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yLowerName?: string = undefined; - - @Validate(STRING, { optional: true }) - yUpperKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yUpperName?: string = undefined; - - @Validate(STRING, { optional: true }) - xLowerKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xLowerName?: string = undefined; - - @Validate(STRING, { optional: true }) - xUpperKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xUpperName?: string = undefined; - - @Validate(BOOLEAN, { optional: true }) - visible?: boolean = true; - - @Validate(COLOR_STRING, { optional: true }) - stroke? = 'black'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth?: number = 1; - - @Validate(RATIO, { optional: true }) - strokeOpacity?: number = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[]; - - @Validate(POSITIVE_NUMBER, { optional: true }) - lineDashOffset?: number; - - @Validate(FUNCTION, { optional: true }) - formatter?: ErrorBarFormatter = undefined; - - cap: ErrorBarCap = new ErrorBarCap(); - +export class ErrorBars extends _ModuleSupport.BaseModuleInstance implements _ModuleSupport.SeriesOptionInstance { private readonly cartesianSeries: ErrorBoundCartesianSeries; private readonly groupNode: ErrorBarGroup; private readonly selection: _Scene.Selection; + readonly properties = new ErrorBarProperties(); + private dataModel?: AnyDataModel; private processedData?: AnyProcessedData; constructor(ctx: _ModuleSupport.SeriesContext) { super(); - this.cartesianSeries = toErrorBoundCartesianSeries(ctx); - const { annotationGroup, annotationSelections } = this.cartesianSeries; + const series = toErrorBoundCartesianSeries(ctx); + const { annotationGroup, annotationSelections } = series; + this.cartesianSeries = series; this.groupNode = new ErrorBarGroup({ name: `${annotationGroup.id}-errorBars`, zIndex: _ModuleSupport.Layers.SERIES_LAYER_ZINDEX, - zIndexSubOrder: this.cartesianSeries.getGroupZIndexSubOrder('annotation'), + zIndexSubOrder: series.getGroupZIndexSubOrder('annotation'), }); + annotationGroup.appendChild(this.groupNode); this.selection = _Scene.Selection.select(this.groupNode, () => this.errorBarFactory()); annotationSelections.add(this.selection); - const series = this.cartesianSeries; this.destroyFns.push( series.addListener('data-processed', (e: SeriesDataProcessedEvent) => this.onDataProcessed(e)), series.addListener('data-update', (e: SeriesDataUpdateEvent) => this.onDataUpdate(e)), @@ -193,12 +99,10 @@ export class ErrorBars getDomain(direction: _ModuleSupport.ChartAxisDirection): any[] { const { xLowerKey, xUpperKey, xErrorsID, yLowerKey, yUpperKey, yErrorsID } = this.getMaybeFlippedKeys(); - let hasAxisErrors: boolean = false; - if (direction == ChartAxisDirection.X) { - hasAxisErrors = xLowerKey !== undefined && xUpperKey != undefined; - } else { - hasAxisErrors = yLowerKey !== undefined && yUpperKey != undefined; - } + const hasAxisErrors = + direction === ChartAxisDirection.X + ? isDefined(xLowerKey) && isDefined(xUpperKey) + : isDefined(yLowerKey) && isDefined(yUpperKey); if (hasAxisErrors) { const { dataModel, processedData, cartesianSeries } = this; @@ -215,7 +119,7 @@ export class ErrorBars private onDataUpdate(event: SeriesDataUpdateEvent) { this.dataModel = event.dataModel; this.processedData = event.processedData; - if (event.dataModel !== undefined && event.processedData !== undefined) { + if (isDefined(event.dataModel) && isDefined(event.processedData)) { this.createNodeData(); this.update(); } @@ -232,6 +136,7 @@ export class ErrorBars const nodeData = this.getNodeData(); const xScale = this.cartesianSeries.axes[ChartAxisDirection.X]?.scale; const yScale = this.cartesianSeries.axes[ChartAxisDirection.Y]?.scale; + if (!xScale || !yScale || !nodeData) { return; } @@ -239,15 +144,14 @@ export class ErrorBars for (let i = 0; i < nodeData.length; i++) { const { midPoint, xLower, xUpper, yLower, yUpper } = this.getDatum(nodeData, i); if (midPoint !== undefined) { - let xBar = undefined; - let yBar = undefined; - if (xLower !== undefined && xUpper !== undefined) { + let xBar, yBar; + if (isDefined(xLower) && isDefined(xUpper)) { xBar = { lowerPoint: { x: this.convert(xScale, xLower), y: midPoint.y }, upperPoint: { x: this.convert(xScale, xUpper), y: midPoint.y }, }; } - if (yLower !== undefined && yUpper !== undefined) { + if (isDefined(yLower) && isDefined(yUpper)) { yBar = { lowerPoint: { x: midPoint.x, y: this.convert(yScale, yLower) }, upperPoint: { x: midPoint.x, y: this.convert(yScale, yUpper) }, @@ -260,7 +164,7 @@ export class ErrorBars } private getMaybeFlippedKeys() { - let { xLowerKey, xUpperKey, yLowerKey, yUpperKey } = this; + let { xLowerKey, xUpperKey, yLowerKey, yUpperKey } = this.properties; let [xErrorsID, yErrorsID] = ['xValue-errors', 'yValue-errors']; if (this.cartesianSeries.shouldFlipXY()) { [xLowerKey, yLowerKey] = [yLowerKey, xLowerKey]; @@ -276,10 +180,10 @@ export class ErrorBars return { midPoint: datum.midPoint, - xLower: datum.datum[xLowerKey ?? ''] ?? undefined, - xUpper: datum.datum[xUpperKey ?? ''] ?? undefined, - yLower: datum.datum[yLowerKey ?? ''] ?? undefined, - yUpper: datum.datum[yUpperKey ?? ''] ?? undefined, + xLower: datum.datum[xLowerKey ?? ''], + xUpper: datum.datum[xUpperKey ?? ''], + yLower: datum.datum[yLowerKey ?? ''], + yUpper: datum.datum[yUpperKey ?? ''], }; } @@ -291,15 +195,14 @@ export class ErrorBars private update() { const nodeData = this.getNodeData(); if (nodeData !== undefined) { - this.selection.update(nodeData, undefined, undefined); + this.selection.update(nodeData); this.selection.each((node, datum, i) => this.updateNode(node, datum, i)); } } private updateNode(node: ErrorBarNode, datum: ErrorBarNodeDatum, _index: number) { - const style = this.getDefaultStyle(); node.datum = datum; - node.update(style, this, false); + node.update(this.getDefaultStyle(), this.properties, false); node.updateBBoxes(); } @@ -320,23 +223,17 @@ export class ErrorBars } getTooltipParams() { - const { xLowerKey, xUpperKey, yLowerKey, yUpperKey } = this; - let { xLowerName, xUpperName, yLowerName, yUpperName } = this; - xLowerName ??= xLowerKey; - xUpperName ??= xUpperKey; - yLowerName ??= yLowerKey; - yUpperName ??= yUpperKey; - - return { + const { xLowerKey, - xLowerName, xUpperKey, - xUpperName, yLowerKey, - yLowerName, yUpperKey, - yUpperName, - }; + xLowerName = xLowerKey, + xUpperName = xUpperKey, + yLowerName = yLowerKey, + yUpperName = yUpperKey, + } = this.properties; + return { xLowerKey, xLowerName, xUpperKey, xUpperName, yLowerKey, yLowerName, yUpperKey, yUpperName }; } private onToggleSeriesItem(event: SeriesVisibilityEvent): void { @@ -351,7 +248,7 @@ export class ErrorBars stroke: baseStyle.stroke, strokeWidth: baseStyle.strokeWidth, strokeOpacity: baseStyle.strokeOpacity, - cap: mergeDefaults(this.cap, baseStyle), + cap: mergeDefaults(this.properties.cap, baseStyle), }; } @@ -364,7 +261,7 @@ export class ErrorBars return this.makeStyle(this.getWhiskerProperties()); } - private restyleHightlightChange( + private restyleHighlightChange( highlightChange: HighlightNodeDatum, style: AgErrorBarThemeableOptions, highlighted: boolean @@ -379,7 +276,7 @@ export class ErrorBars // data points with error bars). for (let i = 0; i < nodeData.length; i++) { if (highlightChange === nodeData[i]) { - this.selection.nodes()[i].update(style, this, highlighted); + this.selection.nodes()[i].update(style, this.properties, highlighted); break; } } @@ -387,16 +284,15 @@ export class ErrorBars private onHighlightChange(event: _ModuleSupport.HighlightChangeEvent) { const { previousHighlight, currentHighlight } = event; - const { cartesianSeries: thisSeries } = this; - if (currentHighlight?.series === thisSeries) { + if (currentHighlight?.series === this.cartesianSeries) { // Highlight this node: - this.restyleHightlightChange(currentHighlight, this.getHighlightStyle(), true); + this.restyleHighlightChange(currentHighlight, this.getHighlightStyle(), true); } - if (previousHighlight?.series === thisSeries) { - // Unhighlight this node: - this.restyleHightlightChange(previousHighlight, this.getDefaultStyle(), false); + if (previousHighlight?.series === this.cartesianSeries) { + // Remove node highlight: + this.restyleHighlightChange(previousHighlight, this.getDefaultStyle(), false); } this.groupNode.opacity = this.cartesianSeries.getOpacity(); @@ -407,7 +303,7 @@ export class ErrorBars } private getWhiskerProperties(): Omit { - const { stroke, strokeWidth, visible, strokeOpacity, lineDash, lineDashOffset } = this; + const { stroke, strokeWidth, visible, strokeOpacity, lineDash, lineDashOffset } = this.properties; return { stroke, strokeWidth, visible, strokeOpacity, lineDash, lineDashOffset }; } } diff --git a/packages/ag-charts-enterprise/src/features/error-bar/errorBarNode.ts b/packages/ag-charts-enterprise/src/features/error-bar/errorBarNode.ts index 50baa05457..624086187a 100644 --- a/packages/ag-charts-enterprise/src/features/error-bar/errorBarNode.ts +++ b/packages/ag-charts-enterprise/src/features/error-bar/errorBarNode.ts @@ -27,9 +27,9 @@ type CapDefaults = NonNullable; type CapOptions = NonNullable; type CapLengthOptions = Pick; -class HierarchialBBox { +class HierarchicalBBox { // ErrorBarNode can include up to 6 bboxes in total (2 whiskers, 4 caps). This is expensive hit - // testing, therefore we'll use a hierachial bbox structure: `union` is the bbox that includes + // testing, therefore we'll use a hierarchical bbox structure: `union` is the bbox that includes // all the components. public union: BBox; public components: BBox[]; @@ -62,7 +62,7 @@ export class ErrorBarNode extends _Scene.Group { // The ErrorBarNode does not need to handle the 'nearest' interaction range type, we can let the // series class handle that for us. The 'exact' interaction range is the same as having a distance // of 0. Therefore, we only need bounding boxes for number based ranges. - private bboxes: HierarchialBBox; + private bboxes: HierarchicalBBox; protected override _datum?: ErrorBarNodeDatum = undefined; public override get datum(): ErrorBarNodeDatum | undefined { @@ -76,7 +76,7 @@ export class ErrorBarNode extends _Scene.Group { super(); this.whiskerPath = new _Scene.Path(); this.capsPath = new _Scene.Path(); - this.bboxes = new HierarchialBBox([]); + this.bboxes = new HierarchicalBBox([]); this.append([this.whiskerPath, this.capsPath]); } @@ -153,7 +153,6 @@ export class ErrorBarNode extends _Scene.Group { return; } const { whiskerStyle, capsStyle } = this.formatStyles(style, formatters, highlighted); - const { xBar, yBar, capDefaults } = this.datum; const whisker = this.whiskerPath; @@ -170,7 +169,7 @@ export class ErrorBarNode extends _Scene.Group { whisker.path.closePath(); whisker.markDirtyTransform(); - // Errorbar caps stretch out pendicular to the whisker equally on both + // ErrorBar caps stretch out perpendicular to the whisker equally on both // sides, so we want the offset to be half of the total length. this.capLength = this.calculateCapLength(capsStyle ?? {}, capDefaults); const capOffset = this.capLength / 2; @@ -195,25 +194,28 @@ export class ErrorBarNode extends _Scene.Group { updateBBoxes(): void { const { capLength, whiskerPath: whisker, capsPath: caps } = this; - const { components } = this.bboxes; const { yBar, xBar } = this.datum ?? {}; const capOffset = capLength / 2; + const components = []; - components.length = (xBar === undefined ? 0 : 3) + (yBar === undefined ? 0 : 3); - let i = 0; if (yBar !== undefined) { const whiskerHeight = yBar.lowerPoint.y - yBar.upperPoint.y; - components[i++] = new BBox(yBar.lowerPoint.x, yBar.upperPoint.y, whisker.strokeWidth, whiskerHeight); - components[i++] = new BBox(yBar.lowerPoint.x - capOffset, yBar.lowerPoint.y, capLength, caps.strokeWidth); - components[i++] = new BBox(yBar.upperPoint.x - capOffset, yBar.upperPoint.y, capLength, caps.strokeWidth); + components.push( + new BBox(yBar.lowerPoint.x, yBar.upperPoint.y, whisker.strokeWidth, whiskerHeight), + new BBox(yBar.lowerPoint.x - capOffset, yBar.lowerPoint.y, capLength, caps.strokeWidth), + new BBox(yBar.upperPoint.x - capOffset, yBar.upperPoint.y, capLength, caps.strokeWidth) + ); } if (xBar !== undefined) { const whiskerWidth = xBar.upperPoint.x - xBar.lowerPoint.x; - components[i++] = new BBox(xBar.lowerPoint.x, xBar.upperPoint.y, whiskerWidth, whisker.strokeWidth); - components[i++] = new BBox(xBar.lowerPoint.x, xBar.lowerPoint.y - capOffset, caps.strokeWidth, capLength); - components[i++] = new BBox(xBar.upperPoint.x, xBar.upperPoint.y - capOffset, caps.strokeWidth, capLength); + components.push( + new BBox(xBar.lowerPoint.x, xBar.upperPoint.y, whiskerWidth, whisker.strokeWidth), + new BBox(xBar.lowerPoint.x, xBar.lowerPoint.y - capOffset, caps.strokeWidth, capLength), + new BBox(xBar.upperPoint.x, xBar.upperPoint.y - capOffset, caps.strokeWidth, capLength) + ); } + this.bboxes.components = components; this.bboxes.union = BBox.merge(components); } diff --git a/packages/ag-charts-enterprise/src/features/error-bar/errorBarProperties.ts b/packages/ag-charts-enterprise/src/features/error-bar/errorBarProperties.ts new file mode 100644 index 0000000000..6f7da95c9e --- /dev/null +++ b/packages/ag-charts-enterprise/src/features/error-bar/errorBarProperties.ts @@ -0,0 +1,96 @@ +import { type AgErrorBarOptions, _ModuleSupport } from 'ag-charts-community'; + +import type { ErrorBarCapFormatter, ErrorBarFormatter } from './errorBarNode'; + +const { + BaseProperties, + Validate, + BOOLEAN, + COLOR_STRING, + FUNCTION, + LINE_DASH, + NUMBER, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, +} = _ModuleSupport; + +class ErrorBarCap extends BaseProperties> { + @Validate(BOOLEAN, { optional: true }) + visible?: boolean; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number; + + @Validate(RATIO, { optional: true }) + strokeOpacity?: number; + + @Validate(LINE_DASH, { optional: true }) + lineDash?: number[]; + + @Validate(POSITIVE_NUMBER, { optional: true }) + lineDashOffset?: number; + + @Validate(NUMBER, { optional: true }) + length?: number; + + @Validate(RATIO, { optional: true }) + lengthRatio?: number; + + @Validate(FUNCTION, { optional: true }) + formatter?: ErrorBarCapFormatter; +} + +export class ErrorBarProperties extends BaseProperties { + @Validate(STRING, { optional: true }) + yLowerKey?: string; + + @Validate(STRING, { optional: true }) + yLowerName?: string; + + @Validate(STRING, { optional: true }) + yUpperKey?: string; + + @Validate(STRING, { optional: true }) + yUpperName?: string; + + @Validate(STRING, { optional: true }) + xLowerKey?: string; + + @Validate(STRING, { optional: true }) + xLowerName?: string; + + @Validate(STRING, { optional: true }) + xUpperKey?: string; + + @Validate(STRING, { optional: true }) + xUpperName?: string; + + @Validate(BOOLEAN, { optional: true }) + visible?: boolean = true; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string = 'black'; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number = 1; + + @Validate(RATIO, { optional: true }) + strokeOpacity?: number = 1; + + @Validate(LINE_DASH, { optional: true }) + lineDash?: number[]; + + @Validate(POSITIVE_NUMBER, { optional: true }) + lineDashOffset?: number; + + @Validate(FUNCTION, { optional: true }) + formatter?: ErrorBarFormatter; + + @Validate(OBJECT) + cap = new ErrorBarCap(); +} diff --git a/packages/ag-charts-enterprise/src/series/box-plot/boxPlotSeries.ts b/packages/ag-charts-enterprise/src/series/box-plot/boxPlotSeries.ts index 6a0e86d86a..0e07814c5c 100644 --- a/packages/ag-charts-enterprise/src/series/box-plot/boxPlotSeries.ts +++ b/packages/ag-charts-enterprise/src/series/box-plot/boxPlotSeries.ts @@ -1,15 +1,8 @@ -import { - type AgBoxPlotSeriesFormatterParams, - type AgBoxPlotSeriesStyles, - type AgBoxPlotSeriesTooltipRendererParams, - _ModuleSupport, - _Scale, - _Scene, - _Util, -} from 'ag-charts-community'; +import { type AgBoxPlotSeriesStyles, _ModuleSupport, _Scale, _Scene, _Util } from 'ag-charts-community'; import { prepareBoxPlotFromTo, resetBoxPlotSelectionsScalingCenterFn } from './blotPlotUtil'; import { BoxPlotGroup } from './boxPlotGroup'; +import { BoxPlotSeriesProperties } from './boxPlotSeriesProperties'; import type { BoxPlotNodeDatum } from './boxPlotTypes'; const { @@ -18,16 +11,8 @@ const { fixNumericExtent, keyProperty, mergeDefaults, - POSITIVE_NUMBER, - RATIO, - COLOR_STRING, - FUNCTION, - LINE_DASH, - STRING, SeriesNodePickMode, - SeriesTooltip, SMALLEST_KEY_INTERVAL, - Validate, valueProperty, diff, animationValidation, @@ -46,108 +31,20 @@ class BoxPlotSeriesNodeClickEvent< constructor(type: TEvent, nativeEvent: MouseEvent, datum: BoxPlotNodeDatum, series: BoxPlotSeries) { super(type, nativeEvent, datum, series); - this.xKey = series.xKey; - this.minKey = series.minKey; - this.q1Key = series.q1Key; - this.medianKey = series.medianKey; - this.q3Key = series.q3Key; - this.maxKey = series.maxKey; + this.xKey = series.properties.xKey; + this.minKey = series.properties.minKey; + this.q1Key = series.properties.q1Key; + this.medianKey = series.properties.medianKey; + this.q3Key = series.properties.q3Key; + this.maxKey = series.properties.maxKey; } } -class BoxPlotSeriesCap { - @Validate(RATIO) - lengthRatio = 0.5; -} - -class BoxPlotSeriesWhisker { - @Validate(COLOR_STRING, { optional: true }) - stroke?: string; - - @Validate(POSITIVE_NUMBER) - strokeWidth?: number; - - @Validate(RATIO) - strokeOpacity?: number; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset?: number; -} - export class BoxPlotSeries extends _ModuleSupport.AbstractBarSeries { - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(STRING, { optional: true }) - minKey?: string = undefined; - - @Validate(STRING, { optional: true }) - minName?: string = undefined; - - @Validate(STRING, { optional: true }) - q1Key?: string = undefined; - - @Validate(STRING, { optional: true }) - q1Name?: string = undefined; - - @Validate(STRING, { optional: true }) - medianKey?: string = undefined; - - @Validate(STRING, { optional: true }) - medianName?: string = undefined; - - @Validate(STRING, { optional: true }) - q3Key?: string = undefined; - - @Validate(STRING, { optional: true }) - q3Name?: string = undefined; - - @Validate(STRING, { optional: true }) - maxKey?: string = undefined; - - @Validate(STRING, { optional: true }) - maxName?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - fill: string = '#c16068'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = '#333'; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgBoxPlotSeriesFormatterParams) => AgBoxPlotSeriesStyles = undefined; + override properties = new BoxPlotSeriesProperties(); protected override readonly NodeClickEvent = BoxPlotSeriesNodeClickEvent; - readonly cap = new BoxPlotSeriesCap(); - - readonly whisker = new BoxPlotSeriesWhisker(); - - readonly tooltip = new SeriesTooltip(); /** * Used to get the position of items within each group. */ @@ -165,9 +62,11 @@ export class BoxPlotSeries extends _ModuleSupport.AbstractBarSeries { - const { xKey, minKey, q1Key, medianKey, q3Key, maxKey, data = [] } = this; + if (!this.properties.isValid()) { + return; + } - if (!xKey || !minKey || !q1Key || !medianKey || !q3Key || !maxKey) return; + const { xKey, minKey, q1Key, medianKey, q3Key, maxKey } = this.properties; const animationEnabled = !this.ctx.animationManager.isSkipped(); const isContinuousX = this.getCategoryAxis()?.scale instanceof _Scale.ContinuousScale; @@ -179,7 +78,7 @@ export class BoxPlotSeries extends _ModuleSupport.AbstractBarSeries) { - const isVertical = this.direction === 'vertical'; - - motion.resetMotion(datumSelections, resetBoxPlotSelectionsScalingCenterFn(isVertical)); - + const isVertical = this.isVertical(); const { from, to } = prepareBoxPlotFromTo(isVertical); + motion.resetMotion(datumSelections, resetBoxPlotSelectionsScalingCenterFn(isVertical)); motion.staticFromToMotion(this.id, 'datums', this.ctx.animationManager, datumSelections, from, to); } @@ -461,13 +349,13 @@ export class BoxPlotSeries extends _ModuleSupport.AbstractBarSeries { let activeStyles = this.getFormattedStyles(nodeDatum, highlighted); if (highlighted) { - activeStyles = mergeDefaults(this.highlightStyle.item, activeStyles); + activeStyles = mergeDefaults(this.properties.highlightStyle.item, activeStyles); } const { stroke, strokeWidth, strokeOpacity, lineDash, lineDashOffset } = activeStyles; @@ -513,16 +401,10 @@ export class BoxPlotSeries extends _ModuleSupport.AbstractBarSeries { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + minKey!: string; + + @Validate(STRING) + q1Key!: string; + + @Validate(STRING) + medianKey!: string; + + @Validate(STRING) + q3Key!: string; + + @Validate(STRING) + maxKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + minName?: string; + + @Validate(STRING, { optional: true }) + q1Name?: string; + + @Validate(STRING, { optional: true }) + medianName?: string; + + @Validate(STRING, { optional: true }) + q3Name?: string; + + @Validate(STRING, { optional: true }) + maxName?: string; + + @Validate(COLOR_STRING, { optional: true }) + fill: string = '#c16068'; + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(COLOR_STRING) + stroke: string = '#333'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgBoxPlotSeriesFormatterParams) => AgBoxPlotSeriesStyles; + + @Validate(OBJECT) + readonly cap = new BoxPlotSeriesCap(); + + @Validate(OBJECT) + readonly whisker = new BoxPlotSeriesWhisker(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/bullet/bulletModule.ts b/packages/ag-charts-enterprise/src/series/bullet/bulletModule.ts index bef8fcdb1a..be950fac13 100644 --- a/packages/ag-charts-enterprise/src/series/bullet/bulletModule.ts +++ b/packages/ag-charts-enterprise/src/series/bullet/bulletModule.ts @@ -2,7 +2,8 @@ import type { _ModuleSupport } from 'ag-charts-community'; import { _Theme } from 'ag-charts-community'; import { BULLET_DEFAULTS } from './bulletDefaults'; -import { BulletColorRange, BulletSeries } from './bulletSeries'; +import { BulletSeries } from './bulletSeries'; +import { BulletColorRange } from './bulletSeriesProperties'; import { BULLET_SERIES_THEME } from './bulletThemes'; const { CARTESIAN_AXIS_POSITIONS } = _Theme; diff --git a/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.test.ts b/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.test.ts index 12fb7abe85..beafab6567 100644 --- a/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.test.ts +++ b/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.test.ts @@ -293,7 +293,7 @@ describe('BulletSeriesValidation', () => { expect(console.warn).toBeCalledTimes(1); expect(console.warn).toBeCalledWith( - 'AG Charts - Property [colorRanges] of [BulletSeries] cannot be set to [[]]; expecting a non-empty array, ignoring.' + 'AG Charts - Property [colorRanges] of [BulletSeriesProperties] cannot be set to [[]]; expecting a non-empty array, ignoring.' ); await compare(chart, ctx); }); diff --git a/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.ts b/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.ts index 6e1f10d7ae..f880a4f952 100644 --- a/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.ts +++ b/packages/ag-charts-enterprise/src/series/bullet/bulletSeries.ts @@ -1,6 +1,8 @@ -import type { AgBarSeriesStyle, AgBulletSeriesTooltipRendererParams } from 'ag-charts-community'; +import type { AgBarSeriesStyle } from 'ag-charts-community'; import { _ModuleSupport, _Scale, _Scene, _Util } from 'ag-charts-community'; +import { BulletSeriesProperties } from './bulletSeriesProperties'; + const { animationValidation, collapsedStartingBarPosition, @@ -11,13 +13,6 @@ const { resetBarSelectionsFn, seriesLabelFadeInAnimation, valueProperty, - Validate, - COLOR_STRING, - STRING, - LINE_DASH, - POSITIVE_NUMBER, - RATIO, - ARRAY, } = _ModuleSupport; const { fromToMotion } = _Scene.motion; const { sanitizeHtml } = _Util; @@ -37,14 +32,6 @@ interface BulletNodeDatum extends _ModuleSupport.CartesianSeriesNodeDatum { }; } -export class BulletColorRange { - @Validate(COLOR_STRING) - color: string = 'lightgrey'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - stop?: number = undefined; -} - interface NormalizedColorRange { color: string; start: number; @@ -63,82 +50,8 @@ const STYLING_KEYS: (keyof _Scene.Shape)[] = [ type BulletAnimationData = _ModuleSupport.CartesianAnimationData<_Scene.Rect, BulletNodeDatum>; -class TargetStyle { - @Validate(COLOR_STRING) - fill: string = 'black'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(COLOR_STRING) - stroke: string = 'black'; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH) - lineDash: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(RATIO) - lengthRatio: number = 0.75; -} - -class BulletScale { - @Validate(POSITIVE_NUMBER, { optional: true }) - max?: number = undefined; // alias for AgChartOptions.axes[0].max -} - export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, BulletNodeDatum> { - @Validate(COLOR_STRING) - fill: string = 'black'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(COLOR_STRING) - stroke: string = 'black'; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH) - lineDash: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(RATIO) - widthRatio: number = 0.5; - - target: TargetStyle = new TargetStyle(); - - @Validate(STRING) - valueKey: string = ''; - - @Validate(STRING, { optional: true }) - valueName?: string = undefined; - - @Validate(STRING, { optional: true }) - targetKey?: string = undefined; - - @Validate(STRING, { optional: true }) - targetName?: string = undefined; - - @Validate(ARRAY.restrict({ minLength: 1 }), { optional: true }) - colorRanges: BulletColorRange[] = [new BulletColorRange()]; - - scale: BulletScale = new BulletScale(); - - tooltip = new _ModuleSupport.SeriesTooltip(); + override properties = new BulletSeriesProperties(); private normalizedColorRanges: NormalizedColorRange[] = []; private colorRangesGroup: _Scene.Group; @@ -166,21 +79,19 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, } override async processData(dataController: _ModuleSupport.DataController) { - const { valueKey, targetKey, data = [] } = this; - if (!valueKey || !data) return; + if (!this.properties.isValid() || !this.data) { + return; + } + const { valueKey, targetKey } = this.properties; const isContinuousX = _Scale.ContinuousScale.is(this.getCategoryAxis()?.scale); const isContinuousY = _Scale.ContinuousScale.is(this.getValueAxis()?.scale); + const extraProps = []; - const props = [ - keyProperty(this, valueKey, isContinuousX, { id: 'xValue' }), - valueProperty(this, valueKey, isContinuousY, { id: 'value' }), - ]; if (targetKey !== undefined) { - props.push(valueProperty(this, targetKey, isContinuousY, { id: 'target' })); + extraProps.push(valueProperty(this, targetKey, isContinuousY, { id: 'target' })); } - const extraProps = []; if (!this.ctx.animationManager.isSkipped()) { if (this.processedData !== undefined) { extraProps.push(diff(this.processedData)); @@ -189,9 +100,13 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, } // Bullet graphs only need 1 datum, but we keep that `data` option as array for consistency with other series - // types and future compatibility (we may decide to support multiple datums at some point). - await this.requestDataModel(dataController, data.slice(0, 1), { - props: [...props, ...extraProps], + // types and future compatibility (we may decide to support multiple datum at some point). + await this.requestDataModel(dataController, this.data.slice(0, 1), { + props: [ + keyProperty(this, valueKey, isContinuousX, { id: 'xValue' }), + valueProperty(this, valueKey, isContinuousY, { id: 'value' }), + ...extraProps, + ], groupByKeys: true, dataVisible: this.visible, }); @@ -208,11 +123,15 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, } override getSeriesDomain(direction: _ModuleSupport.ChartAxisDirection) { - const { dataModel, processedData, targetKey } = this; - if (!dataModel || !processedData) return []; + const { dataModel, processedData } = this; + if (!dataModel || !processedData) { + return []; + } + + const { valueKey, targetKey, valueName } = this.properties; if (direction === this.getCategoryDirection()) { - return [this.valueName ?? this.valueKey]; + return [valueName ?? valueKey]; } else if (direction == this.getValueAxis()?.direction) { const valueDomain = dataModel.getDomain(this, 'value', 'value', processedData); const targetDomain = @@ -225,20 +144,19 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, override getKeys(direction: _ModuleSupport.ChartAxisDirection): string[] { if (direction === this.getBarDirection()) { - return [this.valueKey]; + return [this.properties.valueKey]; } return super.getKeys(direction); } override async createNodeData() { + const { dataModel, processedData } = this; const { valueKey, targetKey, - dataModel, - processedData, widthRatio, target: { lengthRatio }, - } = this; + } = this.properties; const xScale = this.getCategoryAxis()?.scale; const yScale = this.getValueAxis()?.scale; if (!valueKey || !dataModel || !processedData || !xScale || !yScale) return []; @@ -265,7 +183,7 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, _Util.Logger.warnOnce('negative values are not supported, clipping to 0.'); } - const xValue = this.valueName ?? this.valueKey; + const xValue = this.properties.valueName ?? this.properties.valueKey; const yValue = Math.min(maxValue, Math.max(0, values[0][valueIndex])); const y = yScale.convert(yValue); const barWidth = widthRatio * multiplier; @@ -286,7 +204,7 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, _Util.Logger.warnOnce('negative targets are not supported, ignoring.'); } - if (this.targetKey && values[0][targetIndex] >= 0) { + if (this.properties.targetKey && values[0][targetIndex] >= 0) { const targetLineLength = lengthRatio * multiplier; const targetValue = Math.min(maxValue, values[0][targetIndex]); if (!isNaN(targetValue) && targetValue !== undefined) { @@ -316,7 +234,9 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, context.nodeData.push(nodeData); } - const sortedRanges = [...this.colorRanges].sort((a, b) => (a.stop || maxValue) - (b.stop || maxValue)); + const sortedRanges = [...this.properties.colorRanges].sort( + (a, b) => (a.stop || maxValue) - (b.stop || maxValue) + ); let start = 0; this.normalizedColorRanges = sortedRanges.map((item) => { const stop = Math.min(maxValue, item.stop ?? Infinity); @@ -333,7 +253,7 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, } override getTooltipHtml(nodeDatum: BulletNodeDatum): string { - const { valueKey, valueName, targetKey, targetName } = this; + const { valueKey, valueName, targetKey, targetName } = this.properties; const axis = this.getValueAxis(); const { yValue: valueValue, target: { value: targetValue } = { value: undefined }, datum } = nodeDatum; @@ -352,7 +272,7 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, ? makeLine(valueKey, valueName, valueValue) : `${makeLine(valueKey, valueName, valueValue)}
${makeLine(targetKey, targetName, targetValue)}`; - return this.tooltip.toTooltipHtml( + return this.properties.tooltip.toTooltipHtml( { title, content }, { datum, title, seriesId: this.id, valueKey, valueName, targetKey, targetName } ); @@ -381,13 +301,13 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, // The translation of the rectangles (values) is updated by the animation manager. // The target lines aren't animated, therefore we must update the translation here. for (const { node } of opts.datumSelection) { - const style: AgBarSeriesStyle = this; + const style: AgBarSeriesStyle = this.properties; partialAssign(STYLING_KEYS, node, style); } for (const { node, datum } of this.targetLinesSelection) { if (datum.target !== undefined) { - const style: AgBarSeriesStyle = this.target; + const style: AgBarSeriesStyle = this.properties.target; partialAssign(['x1', 'x2', 'y1', 'y2'], node, datum.target); partialAssign(STYLING_KEYS, node, style); } else { @@ -445,9 +365,7 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, override animateEmptyUpdateReady(data: BulletAnimationData) { const { datumSelections, labelSelections, annotationSelections } = data; - const fns = prepareBarAnimationFunctions( - collapsedStartingBarPosition(this.direction === 'vertical', this.axes) - ); + const fns = prepareBarAnimationFunctions(collapsedStartingBarPosition(this.isVertical(), this.axes)); fromToMotion(this.id, 'nodes', this.ctx.animationManager, datumSelections, fns); seriesLabelFadeInAnimation(this, 'labels', this.ctx.animationManager, labelSelections); @@ -460,9 +378,7 @@ export class BulletSeries extends _ModuleSupport.AbstractBarSeries<_Scene.Rect, this.ctx.animationManager.stopByAnimationGroupId(this.id); const diff = this.processedData?.reduced?.diff; - const fns = prepareBarAnimationFunctions( - collapsedStartingBarPosition(this.direction === 'vertical', this.axes) - ); + const fns = prepareBarAnimationFunctions(collapsedStartingBarPosition(this.isVertical(), this.axes)); fromToMotion( this.id, diff --git a/packages/ag-charts-enterprise/src/series/bullet/bulletSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/bullet/bulletSeriesProperties.ts new file mode 100644 index 0000000000..f8bb0a6482 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/bullet/bulletSeriesProperties.ts @@ -0,0 +1,106 @@ +import type { AgBulletSeriesOptions, AgBulletSeriesTooltipRendererParams } from 'ag-charts-community'; +import { _ModuleSupport } from 'ag-charts-community'; + +const { + BaseProperties, + AbstractBarSeriesProperties, + PropertiesArray, + SeriesTooltip, + Validate, + ARRAY, + COLOR_STRING, + LINE_DASH, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, +} = _ModuleSupport; + +class TargetStyle extends BaseProperties { + @Validate(COLOR_STRING) + fill: string = 'black'; + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(COLOR_STRING) + stroke: string = 'black'; + + @Validate(POSITIVE_NUMBER) + strokeWidth = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(RATIO) + lengthRatio: number = 0.75; +} + +class BulletScale extends BaseProperties { + @Validate(POSITIVE_NUMBER, { optional: true }) + max?: number; // alias for AgChartOptions.axes[0].max +} + +export class BulletColorRange extends BaseProperties { + @Validate(COLOR_STRING) + color: string = 'lightgrey'; + + @Validate(POSITIVE_NUMBER, { optional: true }) + stop?: number; +} + +export class BulletSeriesProperties extends AbstractBarSeriesProperties { + @Validate(STRING) + valueKey!: string; + + @Validate(STRING, { optional: true }) + valueName?: string; + + @Validate(STRING, { optional: true }) + targetKey?: string; + + @Validate(STRING, { optional: true }) + targetName?: string; + + @Validate(COLOR_STRING) + fill: string = 'black'; + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(COLOR_STRING) + stroke: string = 'black'; + + @Validate(POSITIVE_NUMBER) + strokeWidth = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(RATIO) + widthRatio: number = 0.5; + + @Validate(ARRAY.restrict({ minLength: 1 })) + colorRanges: BulletColorRange[] = new PropertiesArray(BulletColorRange).set([{}]); + + @Validate(OBJECT) + readonly target = new TargetStyle(); + + @Validate(OBJECT) + readonly scale = new BulletScale(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeries.ts b/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeries.ts index ebe5bf3926..c6d2e2fe6b 100644 --- a/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeries.ts +++ b/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeries.ts @@ -1,32 +1,10 @@ -import type { - AgHeatmapSeriesFormat, - AgHeatmapSeriesFormatterParams, - AgHeatmapSeriesLabelFormatterParams, - AgHeatmapSeriesTooltipRendererParams, - FontStyle, - FontWeight, - TextAlign, - VerticalAlign, -} from 'ag-charts-community'; +import type { AgHeatmapSeriesFormat, FontStyle, FontWeight, TextAlign, VerticalAlign } from 'ag-charts-community'; import { _ModuleSupport, _Scale, _Scene, _Util } from 'ag-charts-community'; -import { AutoSizedLabel, formatLabels } from '../util/labelFormatter'; - -const { - AND, - Validate, - SeriesNodePickMode, - valueProperty, - ChartAxisDirection, - COLOR_STRING_ARRAY, - ARRAY, - POSITIVE_NUMBER, - STRING, - FUNCTION, - COLOR_STRING, - TEXT_ALIGN, - VERTICAL_ALIGN, -} = _ModuleSupport; +import { formatLabels } from '../util/labelFormatter'; +import { HeatmapSeriesProperties } from './heatmapSeriesProperties'; + +const { SeriesNodePickMode, valueProperty, ChartAxisDirection } = _ModuleSupport; const { Rect, PointerEvents } = _Scene; const { ColorScale } = _Scale; const { sanitizeHtml, Color, Logger } = _Util; @@ -60,7 +38,7 @@ class HeatmapSeriesNodeClickEvent< constructor(type: TEvent, nativeEvent: MouseEvent, datum: HeatmapNodeDatum, series: HeatmapSeries) { super(type, nativeEvent, datum, series); - this.colorKey = series.colorKey; + this.colorKey = series.properties.colorKey; } } @@ -80,56 +58,12 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H static className = 'HeatmapSeries'; static type = 'heatmap' as const; - protected override readonly NodeClickEvent = HeatmapSeriesNodeClickEvent; - - readonly label = new AutoSizedLabel(); - - @Validate(STRING, { optional: true }) - title?: string = undefined; - - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(STRING, { optional: true }) - colorKey?: string = undefined; - - @Validate(STRING, { optional: true }) - colorName?: string = 'Color'; - - @Validate(AND(COLOR_STRING_ARRAY, ARRAY.restrict({ minLength: 1 }))) - colorRange: string[] = ['black', 'black']; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = 'black'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth: number = 0; - - @Validate(TEXT_ALIGN) - textAlign: TextAlign = 'center'; - - @Validate(VERTICAL_ALIGN) - verticalAlign: VerticalAlign = 'middle'; + override properties = new HeatmapSeriesProperties(); - @Validate(POSITIVE_NUMBER) - itemPadding: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgHeatmapSeriesFormatterParams) => AgHeatmapSeriesFormat = undefined; + protected override readonly NodeClickEvent = HeatmapSeriesNodeClickEvent; readonly colorScale = new ColorScale(); - readonly tooltip = new _ModuleSupport.SeriesTooltip(); - constructor(moduleCtx: _ModuleSupport.ModuleContext) { super({ moduleCtx, @@ -141,22 +75,17 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H } override async processData(dataController: _ModuleSupport.DataController) { - const { xKey = '', yKey = '', axes } = this; - - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - if (!xAxis || !yAxis) { + if (!xAxis || !yAxis || !this.properties.isValid() || !this.data?.length) { return; } - const data = xKey && yKey && this.data ? this.data : []; - + const { xKey, yKey, colorRange, colorKey } = this.properties; const { isContinuousX, isContinuousY } = this.isContinuous(); - const { colorScale, colorRange, colorKey } = this; - - const { dataModel, processedData } = await this.requestDataModel(dataController, data ?? [], { + const { dataModel, processedData } = await this.requestDataModel(dataController, this.data, { props: [ valueProperty(this, xKey, isContinuousX, { id: 'xValue' }), valueProperty(this, yKey, isContinuousY, { id: 'yValue' }), @@ -166,14 +95,14 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H if (this.isColorScaleValid()) { const colorKeyIdx = dataModel.resolveProcessedDataIndexById(this, 'colorValue').index; - colorScale.domain = processedData.domain.values[colorKeyIdx]; - colorScale.range = colorRange; - colorScale.update(); + this.colorScale.domain = processedData.domain.values[colorKeyIdx]; + this.colorScale.range = colorRange; + this.colorScale.update(); } } private isColorScaleValid() { - const { colorKey } = this; + const { colorKey } = this.properties; if (!colorKey) { return false; } @@ -218,28 +147,28 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H return []; } + const { + xKey, + xName, + yKey, + yName, + colorKey, + colorName, + textAlign, + verticalAlign, + itemPadding, + colorRange, + label, + } = this.properties; + const xDataIdx = dataModel.resolveProcessedDataIndexById(this, `xValue`).index; const yDataIdx = dataModel.resolveProcessedDataIndexById(this, `yValue`).index; - const colorDataIdx = this.colorKey - ? dataModel.resolveProcessedDataIndexById(this, `colorValue`).index - : undefined; + const colorDataIdx = colorKey ? dataModel.resolveProcessedDataIndexById(this, `colorValue`).index : undefined; const xScale = xAxis.scale; const yScale = yAxis.scale; const xOffset = (xScale.bandwidth ?? 0) / 2; const yOffset = (yScale.bandwidth ?? 0) / 2; - const { - colorScale, - xKey = '', - xName = '', - yKey = '', - yName = '', - colorKey = '', - colorName = '', - textAlign, - verticalAlign, - itemPadding, - } = this; const colorScaleValid = this.isColorScaleValid(); const nodeData: HeatmapNodeDatum[] = []; const labelData: HeatmapLabelDatum[] = []; @@ -259,11 +188,11 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H const y = yScale.convert(yDatum) + yOffset; const colorValue = colorDataIdx != null ? values[colorDataIdx] : undefined; - const fill = colorScaleValid && colorValue != null ? colorScale.convert(colorValue) : this.colorRange[0]; + const fill = colorScaleValid && colorValue != null ? this.colorScale.convert(colorValue) : colorRange[0]; const labelText = colorValue != null - ? this.getLabelText(this.label, { + ? this.getLabelText(label, { value: colorValue, datum, colorKey, @@ -277,9 +206,9 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H const labels = formatLabels( labelText, - this.label, + this.properties.label, undefined, - this.label, + this.properties.label, { padding: itemPadding }, sizeFittingHeight ); @@ -304,7 +233,7 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H if (labels?.label != null) { const { text, fontSize, lineHeight, height: labelHeight } = labels.label; - const { fontStyle, fontFamily, fontWeight, color } = this.label; + const { fontStyle, fontFamily, fontWeight, color } = this.properties.label; const x = point.x + textAlignFactor * (width - 2 * itemPadding); const y = point.y + verticalAlignFactor * (height - 2 * itemPadding) - (labels.height - labelHeight) * 0.5; @@ -330,7 +259,7 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H return [ { - itemId: this.yKey ?? this.id, + itemId: this.properties.yKey ?? this.id, nodeData, labelData, scales: super.calculateScaling(), @@ -356,11 +285,14 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H datumSelection: _Scene.Selection<_Scene.Rect, HeatmapNodeDatum>; isHighlight: boolean; }) { - const { datumSelection, isHighlight: isDatumHighlighted } = opts; - + const { isHighlight: isDatumHighlighted } = opts; const { - xKey = '', - yKey = '', + id: seriesId, + ctx: { callbackCache }, + } = this; + const { + xKey, + yKey, colorKey, formatter, highlightStyle: { @@ -371,27 +303,26 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H fillOpacity: highlightedFillOpacity, }, }, - id: seriesId, - ctx: { callbackCache }, - } = this; + } = this.properties; const xAxis = this.axes[ChartAxisDirection.X]; const [visibleMin, visibleMax] = xAxis?.visibleRange ?? []; const isZoomed = visibleMin !== 0 || visibleMax !== 1; const crisp = !isZoomed; - datumSelection.each((rect, datum) => { + opts.datumSelection.each((rect, datum) => { const { point, width, height } = datum; const fill = isDatumHighlighted && highlightedFill !== undefined ? Color.interpolate(datum.fill, highlightedFill)(highlightedFillOpacity ?? 1) : datum.fill; - const stroke = isDatumHighlighted && highlightedStroke !== undefined ? highlightedStroke : this.stroke; + const stroke = + isDatumHighlighted && highlightedStroke !== undefined ? highlightedStroke : this.properties.stroke; const strokeWidth = isDatumHighlighted && highlightedDatumStrokeWidth !== undefined ? highlightedDatumStrokeWidth - : this.strokeWidth; + : this.properties.strokeWidth; let format: AgHeatmapSeriesFormat | undefined; if (formatter) { @@ -424,7 +355,7 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H labelSelection: _Scene.Selection<_Scene.Text, HeatmapLabelDatum>; }) { const { labelData, labelSelection } = opts; - const { enabled } = this.label; + const { enabled } = this.properties.label; const data = enabled ? labelData : []; return labelSelection.update(data); @@ -452,37 +383,23 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H } getTooltipHtml(nodeDatum: HeatmapNodeDatum): string { - const { xKey, yKey, axes } = this; - - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - if (!xKey || !yKey || !xAxis || !yAxis) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } + const { xKey, yKey, colorKey, xName, yName, colorName, stroke, strokeWidth, colorRange, formatter, tooltip } = + this.properties; const { - formatter, - tooltip, - xName, - yName, - id: seriesId, - stroke, - strokeWidth, - colorKey, - colorName, colorScale, + id: seriesId, ctx: { callbackCache }, } = this; - const { - datum, - xValue, - yValue, - colorValue, - // label: { text: labelText }, - } = nodeDatum; - const fill = this.isColorScaleValid() ? colorScale.convert(colorValue) : this.colorRange[0]; + const { datum, xValue, yValue, colorValue } = nodeDatum; + const fill = this.isColorScaleValid() ? colorScale.convert(colorValue) : colorRange[0]; let format: AgHeatmapSeriesFormat | undefined; @@ -501,7 +418,7 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H } const color = format?.fill ?? fill ?? 'gray'; - const title = this.title ?? yName; + const title = this.properties.title ?? yName; const xString = sanitizeHtml(xAxis.formatDatum(xValue)); const yString = sanitizeHtml(yAxis.formatDatum(yValue)); @@ -530,9 +447,13 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H } getLegendData(legendType: _ModuleSupport.ChartLegendType): _ModuleSupport.GradientLegendDatum[] { - const { data, dataModel, xKey, yKey } = this; - - if (!(data?.length && xKey && yKey && dataModel && legendType === 'gradient' && this.isColorScaleValid())) { + if ( + legendType !== 'gradient' || + !this.data?.length || + !this.properties.isValid() || + !this.isColorScaleValid() || + !this.dataModel + ) { return []; } @@ -541,18 +462,18 @@ export class HeatmapSeries extends _ModuleSupport.CartesianSeries<_Scene.Rect, H legendType: 'gradient', enabled: this.visible, seriesId: this.id, - colorName: this.colorName, + colorName: this.properties.colorName, colorDomain: this.processedData!.domain.values[ - dataModel.resolveProcessedDataIndexById(this, 'colorValue').index + this.dataModel.resolveProcessedDataIndexById(this, 'colorValue').index ], - colorRange: this.colorRange, + colorRange: this.properties.colorRange, }, ]; } protected isLabelEnabled() { - return this.label.enabled && Boolean(this.colorKey); + return this.properties.label.enabled && Boolean(this.properties.colorKey); } override getBandScalePadding() { diff --git a/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeriesProperties.ts new file mode 100644 index 0000000000..5ab2b7a8d4 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/heatmap/heatmapSeriesProperties.ts @@ -0,0 +1,78 @@ +import type { + AgHeatmapSeriesFormat, + AgHeatmapSeriesFormatterParams, + AgHeatmapSeriesLabelFormatterParams, + AgHeatmapSeriesOptions, + AgHeatmapSeriesTooltipRendererParams, + TextAlign, + VerticalAlign, +} from 'ag-charts-community'; +import { _ModuleSupport } from 'ag-charts-community'; + +import { AutoSizedLabel } from '../util/labelFormatter'; + +const { + SeriesProperties, + SeriesTooltip, + Validate, + AND, + ARRAY, + COLOR_STRING, + COLOR_STRING_ARRAY, + FUNCTION, + OBJECT, + POSITIVE_NUMBER, + STRING, + TEXT_ALIGN, + VERTICAL_ALIGN, +} = _ModuleSupport; + +export class HeatmapSeriesProperties extends SeriesProperties { + @Validate(STRING, { optional: true }) + title?: string; + + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING, { optional: true }) + colorKey?: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + colorName?: string = 'Color'; + + @Validate(AND(COLOR_STRING_ARRAY, ARRAY.restrict({ minLength: 1 }))) + colorRange: string[] = ['black', 'black']; + + @Validate(COLOR_STRING, { optional: true }) + stroke: string = 'black'; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth: number = 0; + + @Validate(TEXT_ALIGN) + textAlign: TextAlign = 'center'; + + @Validate(VERTICAL_ALIGN) + verticalAlign: VerticalAlign = 'middle'; + + @Validate(POSITIVE_NUMBER) + itemPadding: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgHeatmapSeriesFormatterParams) => AgHeatmapSeriesFormat; + + @Validate(OBJECT) + readonly label = new AutoSizedLabel(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/nightingale/nightingaleSeries.ts b/packages/ag-charts-enterprise/src/series/nightingale/nightingaleSeries.ts index 58a8a4f2bc..135eb240e9 100644 --- a/packages/ag-charts-enterprise/src/series/nightingale/nightingaleSeries.ts +++ b/packages/ag-charts-enterprise/src/series/nightingale/nightingaleSeries.ts @@ -1,7 +1,13 @@ -import { type AgRadialSeriesFormat, type _ModuleSupport, _Scene } from 'ag-charts-community'; +import { + type AgNightingaleSeriesOptions, + type AgRadialSeriesFormat, + type _ModuleSupport, + _Scene, +} from 'ag-charts-community'; import type { RadialColumnNodeDatum } from '../radial-column/radialColumnSeriesBase'; import { RadialColumnSeriesBase } from '../radial-column/radialColumnSeriesBase'; +import { RadialColumnSeriesBaseProperties } from '../radial-column/radialColumnSeriesBaseProperties'; import { prepareNightingaleAnimationFunctions, resetNightingaleSelectionFn } from './nightingaleUtil'; const { Sector } = _Scene; @@ -10,6 +16,8 @@ export class NightingaleSeries extends RadialColumnSeriesBase<_Scene.Sector> { static className = 'NightingaleSeries'; static type = 'nightingale' as const; + override properties = new RadialColumnSeriesBaseProperties(); + // TODO: Enable once the options contract has been revisited // @Validate(POSITIVE_NUMBER) // sectorSpacing = 1; diff --git a/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeries.ts b/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeries.ts index 2b1808a1eb..dc7f67a598 100644 --- a/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeries.ts +++ b/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeries.ts @@ -1,22 +1,18 @@ -import { _ModuleSupport, _Scene, _Util } from 'ag-charts-community'; +import { _ModuleSupport, _Scene } from 'ag-charts-community'; import { type RadarPathPoint, RadarSeries } from '../radar/radarSeries'; - -const { RATIO, COLOR_STRING, Validate, ChartAxisDirection } = _ModuleSupport; +import { RadarAreaSeriesProperties } from './radarAreaSeriesProperties'; const { Group, Path, PointerEvents, Selection } = _Scene; +const { ChartAxisDirection } = _ModuleSupport; export class RadarAreaSeries extends RadarSeries { static override className = 'RadarAreaSeries'; static type = 'radar-area' as const; - protected areaSelection: _Scene.Selection<_Scene.Path, boolean>; - - @Validate(COLOR_STRING, { optional: true }) - fill?: string = 'black'; + override properties = new RadarAreaSeriesProperties(); - @Validate(RATIO) - fillOpacity = 1; + protected areaSelection: _Scene.Selection<_Scene.Path, boolean>; constructor(moduleCtx: _ModuleSupport.ModuleContext) { super(moduleCtx); @@ -37,15 +33,15 @@ export class RadarAreaSeries extends RadarSeries { } protected override getMarkerFill(highlightedStyle?: _ModuleSupport.SeriesItemHighlightStyle) { - return highlightedStyle?.fill ?? this.marker.fill ?? this.fill; + return highlightedStyle?.fill ?? this.properties.marker.fill ?? this.properties.fill; } protected override beforePathAnimation() { super.beforePathAnimation(); const areaNode = this.getAreaNode(); - areaNode.fill = this.fill; - areaNode.fillOpacity = this.fillOpacity; + areaNode.fill = this.properties.fill; + areaNode.fillOpacity = this.properties.fillOpacity; areaNode.pointerEvents = PointerEvents.None; areaNode.stroke = undefined; } @@ -86,12 +82,12 @@ export class RadarAreaSeries extends RadarSeries { const { path: areaPath } = areaNode; const areaPoints = this.getAreaPoints({ breakMissingPoints: false }); - areaNode.fill = this.fill; - areaNode.fillOpacity = this.fillOpacity; + areaNode.fill = this.properties.fill; + areaNode.fillOpacity = this.properties.fillOpacity; areaNode.stroke = undefined; - areaNode.lineDash = this.lineDash; - areaNode.lineDashOffset = this.lineDashOffset; + areaNode.lineDash = this.properties.lineDash; + areaNode.lineDashOffset = this.properties.lineDashOffset; areaNode.lineJoin = areaNode.lineCap = 'round'; areaPath.clear({ trackChanges: true }); diff --git a/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeriesProperties.ts new file mode 100644 index 0000000000..5848014484 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/radar-area/radarAreaSeriesProperties.ts @@ -0,0 +1,13 @@ +import { AgRadarAreaSeriesOptions, _ModuleSupport, _Scene } from 'ag-charts-community'; + +import { RadarSeriesProperties } from '../radar/radarSeriesProperties'; + +const { RATIO, COLOR_STRING, Validate } = _ModuleSupport; + +export class RadarAreaSeriesProperties extends RadarSeriesProperties { + @Validate(COLOR_STRING) + fill: string = 'black'; + + @Validate(RATIO) + fillOpacity = 1; +} diff --git a/packages/ag-charts-enterprise/src/series/radar/radarSeries.ts b/packages/ag-charts-enterprise/src/series/radar/radarSeries.ts index 881c646190..c2cda1495b 100644 --- a/packages/ag-charts-enterprise/src/series/radar/radarSeries.ts +++ b/packages/ag-charts-enterprise/src/series/radar/radarSeries.ts @@ -1,25 +1,11 @@ -import type { - AgPieSeriesFormat, - AgPieSeriesFormatterParams, - AgPieSeriesTooltipRendererParams, - AgRadarSeriesLabelFormatterParams, - AgRadialSeriesOptionsKeys, -} from 'ag-charts-community'; import { _ModuleSupport, _Scene, _Util } from 'ag-charts-community'; +import { RadarSeriesProperties } from './radarSeriesProperties'; + const { ChartAxisDirection, - HighlightStyle, - DEGREE, - COLOR_STRING, - FUNCTION, - LINE_DASH, - STRING, - RATIO, - POSITIVE_NUMBER, PolarAxis, SeriesNodePickMode, - Validate, valueProperty, fixNumericExtent, seriesLabelFadeInAnimation, @@ -49,12 +35,12 @@ class RadarSeriesNodeClickEvent< readonly radiusKey?: string; constructor(type: TEvent, nativeEvent: MouseEvent, datum: RadarNodeDatum, series: RadarSeries) { super(type, nativeEvent, datum, series); - this.angleKey = series.angleKey; - this.radiusKey = series.radiusKey; + this.angleKey = series.properties.angleKey; + this.radiusKey = series.properties.radiusKey; } } -interface RadarNodeDatum extends _ModuleSupport.SeriesNodeDatum { +export interface RadarNodeDatum extends _ModuleSupport.SeriesNodeDatum { readonly label?: { text: string; x: number; @@ -69,64 +55,14 @@ interface RadarNodeDatum extends _ModuleSupport.SeriesNodeDatum { export abstract class RadarSeries extends _ModuleSupport.PolarSeries { static className = 'RadarSeries'; - protected override readonly NodeClickEvent = RadarSeriesNodeClickEvent; + override properties = new RadarSeriesProperties(); - readonly marker = new _ModuleSupport.SeriesMarker(); - readonly label = new _Scene.Label(); + protected override readonly NodeClickEvent = RadarSeriesNodeClickEvent; protected lineSelection: _Scene.Selection<_Scene.Path, boolean>; protected nodeData: RadarNodeDatum[] = []; - tooltip = new _ModuleSupport.SeriesTooltip(); - - /** - * The key of the numeric field to use to determine the angle (for example, - * a pie sector angle). - */ - @Validate(STRING) - angleKey = ''; - - @Validate(STRING, { optional: true }) - angleName?: string = undefined; - - /** - * The key of the numeric field to use to determine the radii of pie sectors. - * The largest value will correspond to the full radius and smaller values to - * proportionally smaller radii. - */ - @Validate(STRING) - radiusKey: string = ''; - - @Validate(STRING, { optional: true }) - radiusName?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = 'black'; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgPieSeriesFormatterParams) => AgPieSeriesFormat = undefined; - - /** - * The series rotation in degrees. - */ - @Validate(DEGREE) - rotation = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 1; - - override readonly highlightStyle = new HighlightStyle(); - constructor(moduleCtx: _ModuleSupport.ModuleContext) { super({ moduleCtx, @@ -145,7 +81,7 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries(dataController, data, { + await this.requestDataModel(dataController, this.data ?? [], { props: [ valueProperty(this, angleKey, false, { id: 'angleValue' }), valueProperty(this, radiusKey, false, { id: 'radiusValue', invalidValue: undefined }), @@ -221,12 +157,13 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries (isNumber(value) ? value.toFixed(2) : String(value)) ); if (labelText) { labelNodeDatum = { - x: x + cos * this.marker.size, - y: y + sin * this.marker.size, + x: x + cos * marker.size, + y: y + sin * marker.size, text: labelText, textAlign: isNumberEqual(cos, 0) ? 'center' : cos > 0 ? 'left' : 'right', textBaseline: isNumberEqual(sin, 0) ? 'middle' : sin > 0 ? 'top' : 'bottom', @@ -282,7 +212,7 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries, highlight: boolean) { - const { marker, visible, ctx, angleKey, radiusKey, id: seriesId } = this; - const { shape, enabled, formatter, size } = marker; - const { callbackCache } = ctx; + const { angleKey, radiusKey, marker, visible } = this.properties; + let selectionData: RadarNodeDatum[] = []; - if (visible && shape && enabled) { + + if (visible && marker.shape && marker.enabled) { if (highlight) { const highlighted = this.ctx.highlightManager?.getActiveHighlight(); if (highlighted?.datum) { @@ -346,29 +276,29 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries { const fill = this.getMarkerFill(highlightedStyle); - const stroke = highlightedStyle?.stroke ?? marker.stroke ?? this.stroke; - const strokeWidth = highlightedStyle?.strokeWidth ?? marker.strokeWidth ?? this.strokeWidth ?? 1; - const format = formatter - ? callbackCache.call(formatter, { + const stroke = highlightedStyle?.stroke ?? marker.stroke ?? this.properties.stroke; + const strokeWidth = highlightedStyle?.strokeWidth ?? marker.strokeWidth ?? this.properties.strokeWidth ?? 1; + const format = marker.formatter + ? this.ctx.callbackCache.call(marker.formatter, { datum: datum.datum, angleKey, radiusKey, fill, stroke, strokeWidth, - size, + size: marker.size, highlighted: highlight, - seriesId, + seriesId: this.id, }) : undefined; node.fill = format?.fill ?? fill; node.stroke = format?.stroke ?? stroke; node.strokeWidth = format?.strokeWidth ?? strokeWidth; node.fillOpacity = highlightedStyle?.fillOpacity ?? marker.fillOpacity ?? 1; - node.strokeOpacity = marker.strokeOpacity ?? this.strokeOpacity ?? 1; + node.strokeOpacity = marker.strokeOpacity ?? this.properties.strokeOpacity ?? 1; node.size = format?.size ?? marker.size; const { x, y } = datum.point!; @@ -379,8 +309,8 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries { + const { label } = this.properties; + this.labelSelection.update(this.nodeData).each((node, datum) => { if (label.enabled && datum.label) { node.x = datum.label.x; node.y = datum.label.y; @@ -403,21 +333,21 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries radius + marker.size) { + if (distanceFromCenter > radius + this.properties.marker.size) { return; } @@ -520,7 +450,7 @@ export abstract class RadarSeries extends _ModuleSupport.PolarSeries extends SeriesProperties { + @Validate(STRING) + angleKey!: string; + + @Validate(STRING) + radiusKey!: string; + + @Validate(STRING, { optional: true }) + angleName?: string; + + @Validate(STRING, { optional: true }) + radiusName?: string; + + @Validate(COLOR_STRING) + stroke: string = 'black'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgPieSeriesFormatterParams) => AgPieSeriesFormat; + + @Validate(DEGREE) + rotation: number = 0; + + @Validate(OBJECT) + readonly marker = new SeriesMarker(); + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/radial-bar/radialBarSeries.ts b/packages/ag-charts-enterprise/src/series/radial-bar/radialBarSeries.ts index cff14e2719..4411a3338f 100644 --- a/packages/ag-charts-enterprise/src/series/radial-bar/radialBarSeries.ts +++ b/packages/ag-charts-enterprise/src/series/radial-bar/radialBarSeries.ts @@ -1,29 +1,15 @@ -import type { - AgRadialSeriesFormat, - AgRadialSeriesFormatterParams, - AgRadialSeriesLabelFormatterParams, - AgRadialSeriesTooltipRendererParams, -} from 'ag-charts-community'; import { _ModuleSupport, _Scale, _Scene, _Util } from 'ag-charts-community'; import { RadiusCategoryAxis } from '../../axes/radius-category/radiusCategoryAxis'; import type { RadialColumnNodeDatum } from '../radial-column/radialColumnSeriesBase'; +import { RadialBarSeriesProperties } from './radialBarSeriesProperties'; import { prepareRadialBarSeriesAnimationFunctions, resetRadialBarSelectionsFn } from './radialBarUtil'; const { ChartAxisDirection, - HighlightStyle, - COLOR_STRING, - DEGREE, - FUNCTION, - LINE_DASH, - NUMBER, - POSITIVE_NUMBER, - STRING, - RATIO, PolarAxis, - Validate, diff, + isDefined, groupAccumulativeValueProperty, keyProperty, normaliseGroupTo, @@ -46,8 +32,8 @@ class RadialBarSeriesNodeClickEvent< readonly radiusKey?: string; constructor(type: TEvent, nativeEvent: MouseEvent, datum: RadialBarNodeDatum, series: RadialBarSeries) { super(type, nativeEvent, datum, series); - this.angleKey = series.angleKey; - this.radiusKey = series.radiusKey; + this.angleKey = series.properties.angleKey; + this.radiusKey = series.properties.radiusKey; } } @@ -74,61 +60,12 @@ export class RadialBarSeries extends _ModuleSupport.PolarSeries(); + protected override readonly NodeClickEvent = RadialBarSeriesNodeClickEvent; protected nodeData: RadialBarNodeDatum[] = []; - tooltip = new _ModuleSupport.SeriesTooltip(); - - @Validate(STRING) - angleKey = ''; - - @Validate(STRING, { optional: true }) - angleName?: string = undefined; - - @Validate(STRING) - radiusKey: string = ''; - - @Validate(STRING, { optional: true }) - radiusName?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - fill?: string = 'black'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = 'black'; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgRadialSeriesFormatterParams) => AgRadialSeriesFormat = undefined; - - @Validate(DEGREE) - rotation = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 1; - - @Validate(STRING, { optional: true }) - stackGroup?: string = undefined; - - @Validate(NUMBER, { optional: true }) - normalizedTo?: number; - - override readonly highlightStyle = new HighlightStyle(); - private groupScale = new BandScale(); constructor(moduleCtx: _ModuleSupport.ModuleContext) { @@ -169,32 +106,33 @@ export class RadialBarSeries extends _ModuleSupport.PolarSeries(dataController, data, { + await this.requestDataModel(dataController, this.data ?? [], { props: [ keyProperty(this, radiusKey, false, { id: 'radiusValue' }), valueProperty(this, angleKey, true, { @@ -250,9 +188,9 @@ export class RadialBarSeries extends _ModuleSupport.PolarSeries { const labelText = this.getLabelText( - this.label, - { - value: angleDatum, - datum, - angleKey, - radiusKey, - angleName: this.angleName, - radiusName: this.radiusName, - }, + label, + { value: angleDatum, datum, angleKey, radiusKey, angleName, radiusName }, (value) => (isNumber(value) ? value.toFixed(2) : String(value)) ); if (labelText) { @@ -332,7 +265,9 @@ export class RadialBarSeries extends _ModuleSupport.PolarSeries datum.radiusValue; selection.update(selectionData, undefined, idFn).each((node, datum) => { - const format = this.formatter - ? this.ctx.callbackCache.call(this.formatter, { + const format = this.properties.formatter + ? this.ctx.callbackCache.call(this.properties.formatter, { datum, fill, stroke, strokeWidth, highlighted: highlight, - angleKey: this.angleKey, - radiusKey: this.radiusKey, + angleKey: this.properties.angleKey, + radiusKey: this.properties.radiusKey, seriesId: this.id, }) : undefined; @@ -417,7 +352,7 @@ export class RadialBarSeries extends _ModuleSupport.PolarSeries { + const { label } = this.properties; + this.labelSelection.update(this.nodeData).each((node, datum) => { if (label.enabled && datum.label) { node.x = datum.label.x; node.y = datum.label.y; @@ -473,26 +408,15 @@ export class RadialBarSeries extends _ModuleSupport.PolarSeries extends SeriesProperties { + @Validate(STRING) + angleKey!: string; + + @Validate(STRING) + radiusKey!: string; + + @Validate(STRING, { optional: true }) + angleName?: string; + + @Validate(STRING, { optional: true }) + radiusName?: string; + + @Validate(COLOR_STRING) + fill: string = 'black'; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING) + stroke: string = 'black'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgRadialSeriesFormatterParams) => AgRadialSeriesFormat; + + @Validate(DEGREE) + rotation: number = 0; + + @Validate(STRING, { optional: true }) + stackGroup?: string; + + @Validate(NUMBER, { optional: true }) + normalizedTo?: number; + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeries.ts b/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeries.ts index 5194d4638f..c97034262b 100644 --- a/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeries.ts +++ b/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeries.ts @@ -2,20 +2,17 @@ import { _ModuleSupport } from 'ag-charts-community'; import type { RadialColumnNodeDatum } from './radialColumnSeriesBase'; import { RadialColumnSeriesBase } from './radialColumnSeriesBase'; +import { RadialColumnSeriesProperties } from './radialColumnSeriesProperties'; import { RadialColumnShape, getRadialColumnWidth } from './radialColumnShape'; import { prepareRadialColumnAnimationFunctions, resetRadialColumnSelectionFn } from './radialColumnUtil'; -const { ChartAxisDirection, RATIO, PolarAxis, Validate } = _ModuleSupport; +const { ChartAxisDirection, PolarAxis } = _ModuleSupport; export class RadialColumnSeries extends RadialColumnSeriesBase { static className = 'RadialColumnSeries'; static type = 'radial-column' as const; - @Validate(RATIO, { optional: true }) - columnWidthRatio?: number; - - @Validate(RATIO, { optional: true }) - maxColumnWidthRatio?: number; + override properties = new RadialColumnSeriesProperties(); constructor(moduleCtx: _ModuleSupport.ModuleContext) { super(moduleCtx, { @@ -60,7 +57,7 @@ export class RadialColumnSeries extends RadialColumnSeriesBase ) { super(type, nativeEvent, datum, series); - this.angleKey = series.angleKey; - this.radiusKey = series.radiusKey; + this.angleKey = series.properties.angleKey; + this.radiusKey = series.properties.radiusKey; } } @@ -82,59 +69,10 @@ export abstract class RadialColumnSeriesBase< > extends _ModuleSupport.PolarSeries { protected override readonly NodeClickEvent = RadialColumnSeriesNodeClickEvent; - readonly label = new _Scene.Label(); + abstract override properties: RadialColumnSeriesBaseProperties; protected nodeData: RadialColumnNodeDatum[] = []; - tooltip = new _ModuleSupport.SeriesTooltip(); - - @Validate(STRING) - angleKey = ''; - - @Validate(STRING, { optional: true }) - angleName?: string = undefined; - - @Validate(STRING) - radiusKey: string = ''; - - @Validate(STRING, { optional: true }) - radiusName?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - fill?: string = 'black'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = 'black'; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgRadialSeriesFormatterParams) => AgRadialSeriesFormat = undefined; - - @Validate(DEGREE) - rotation = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth = 1; - - @Validate(STRING, { optional: true }) - stackGroup?: string = undefined; - - @Validate(NUMBER, { optional: true }) - normalizedTo?: number; - - override readonly highlightStyle = new HighlightStyle(); - private groupScale = new BandScale(); constructor( @@ -185,19 +123,19 @@ export abstract class RadialColumnSeriesBase< protected abstract getStackId(): string; override async processData(dataController: _ModuleSupport.DataController) { - const { data = [], visible } = this; - const { angleKey, radiusKey } = this; - - if (!angleKey || !radiusKey) return; + if (!this.properties.isValid()) { + return; + } const stackGroupId = this.getStackId(); const stackGroupTrailingId = `${stackGroupId}-trailing`; - - const normalizedToAbs = Math.abs(this.normalizedTo ?? NaN); - const normaliseTo = normalizedToAbs && isFinite(normalizedToAbs) ? normalizedToAbs : undefined; + const { angleKey, radiusKey, normalizedTo, visible } = this.properties; const extraProps = []; - if (normaliseTo) { - extraProps.push(normaliseGroupTo(this, [stackGroupId, stackGroupTrailingId], normaliseTo, 'range')); + + if (isDefined(normalizedTo)) { + extraProps.push( + normaliseGroupTo(this, [stackGroupId, stackGroupTrailingId], Math.abs(normalizedTo), 'range') + ); } const animationEnabled = !this.ctx.animationManager.isSkipped(); @@ -208,9 +146,9 @@ export abstract class RadialColumnSeriesBase< extraProps.push(animationValidation(this)); } - const visibleProps = this.visible || !animationEnabled ? {} : { forceValue: 0 }; + const visibleProps = visible || !animationEnabled ? {} : { forceValue: 0 }; - await this.requestDataModel(dataController, data, { + await this.requestDataModel(dataController, this.data ?? [], { props: [ keyProperty(this, angleKey, false, { id: 'angleValue' }), valueProperty(this, radiusKey, true, { @@ -266,9 +204,9 @@ export abstract class RadialColumnSeriesBase< } async createNodeData() { - const { processedData, dataModel, angleKey, radiusKey } = this; + const { processedData, dataModel, groupScale } = this; - if (!processedData || !dataModel || !angleKey || !radiusKey) { + if (!processedData || !dataModel || !this.properties.isValid()) { return []; } @@ -295,7 +233,6 @@ export abstract class RadialColumnSeriesBase< const groupAngleStep = angleScale.bandwidth ?? 0; const paddedGroupAngleStep = groupAngleStep * (1 - groupPaddingOuter); - const { groupScale } = this; const { index: groupIndex, visibleGroupCount } = this.ctx.seriesStateManager.getVisiblePeerGroupIndex(this); groupScale.domain = Array.from({ length: visibleGroupCount }).map((_, i) => String(i)); groupScale.range = [-paddedGroupAngleStep / 2, paddedGroupAngleStep / 2]; @@ -305,6 +242,8 @@ export abstract class RadialColumnSeriesBase< const axisOuterRadius = this.radius; const axisTotalRadius = axisOuterRadius + axisInnerRadius; + const { angleKey, radiusKey, angleName, radiusName, label } = this.properties; + const getLabelNodeDatum = ( datum: RadialColumnNodeDatum, radiusDatum: number, @@ -312,26 +251,13 @@ export abstract class RadialColumnSeriesBase< y: number ): RadialColumnLabelNodeDatum | undefined => { const labelText = this.getLabelText( - this.label, - { - value: radiusDatum, - datum, - angleKey, - radiusKey, - angleName: this.angleName, - radiusName: this.radiusName, - }, + label, + { value: radiusDatum, datum, angleKey, radiusKey, angleName, radiusName }, (value) => (isNumber(value) ? value.toFixed(2) : String(value)) ); if (labelText) { - return { - x, - y, - text: labelText, - textAlign: 'center', - textBaseline: 'middle', - }; + return { x, y, text: labelText, textAlign: 'center', textBaseline: 'middle' }; } }; @@ -358,7 +284,9 @@ export abstract class RadialColumnSeriesBase< const x = cos * midRadius; const y = sin * midRadius; - const labelNodeDatum = this.label.enabled ? getLabelNodeDatum(datum, radiusDatum, x, y) : undefined; + const labelNodeDatum = this.properties.label.enabled + ? getLabelNodeDatum(datum, radiusDatum, x, y) + : undefined; const columnWidth = this.getColumnWidth(startAngle, endAngle); @@ -432,24 +360,24 @@ export abstract class RadialColumnSeriesBase< selectionData = this.nodeData; } - const highlightedStyle = highlight ? this.highlightStyle.item : undefined; - const fill = highlightedStyle?.fill ?? this.fill; - const fillOpacity = highlightedStyle?.fillOpacity ?? this.fillOpacity; - const stroke = highlightedStyle?.stroke ?? this.stroke; - const strokeOpacity = this.strokeOpacity; - const strokeWidth = highlightedStyle?.strokeWidth ?? this.strokeWidth; + const highlightedStyle = highlight ? this.properties.highlightStyle.item : undefined; + const fill = highlightedStyle?.fill ?? this.properties.fill; + const fillOpacity = highlightedStyle?.fillOpacity ?? this.properties.fillOpacity; + const stroke = highlightedStyle?.stroke ?? this.properties.stroke; + const strokeOpacity = this.properties.strokeOpacity; + const strokeWidth = highlightedStyle?.strokeWidth ?? this.properties.strokeWidth; const idFn = (datum: RadialColumnNodeDatum) => datum.angleValue; selection.update(selectionData, undefined, idFn).each((node, datum) => { - const format = this.formatter - ? this.ctx.callbackCache.call(this.formatter, { + const format = this.properties.formatter + ? this.ctx.callbackCache.call(this.properties.formatter, { datum, fill, stroke, strokeWidth, highlighted: highlight, - angleKey: this.angleKey, - radiusKey: this.radiusKey, + angleKey: this.properties.angleKey, + radiusKey: this.properties.radiusKey, seriesId: this.id, }) : undefined; @@ -460,14 +388,14 @@ export abstract class RadialColumnSeriesBase< node.stroke = format?.stroke ?? stroke; node.strokeOpacity = strokeOpacity; node.strokeWidth = format?.strokeWidth ?? strokeWidth; - node.lineDash = this.lineDash; + node.lineDash = this.properties.lineDash; node.lineJoin = 'round'; }); } protected updateLabels() { - const { label, labelSelection } = this; - labelSelection.update(this.nodeData).each((node, datum) => { + const { label } = this.properties; + this.labelSelection.update(this.nodeData).each((node, datum) => { if (label.enabled && datum.label) { node.x = datum.label.x; node.y = datum.label.y; @@ -513,26 +441,15 @@ export abstract class RadialColumnSeriesBase< } getTooltipHtml(nodeDatum: RadialColumnNodeDatum): string { - const { - id: seriesId, - axes, - angleKey, - angleName, - radiusKey, - radiusName, - fill, - formatter, - stroke, - strokeWidth, - tooltip, - dataModel, - } = this; + const { id: seriesId, axes, dataModel } = this; + const { angleKey, radiusKey, angleName, radiusName, fill, stroke, strokeWidth, formatter, tooltip } = + this.properties; const { angleValue, radiusValue, datum } = nodeDatum; const xAxis = axes[ChartAxisDirection.X]; const yAxis = axes[ChartAxisDirection.Y]; - if (!(angleKey && radiusKey) || !(xAxis && yAxis && isNumber(radiusValue)) || !dataModel) { + if (!this.properties.isValid() || !(xAxis && yAxis && isNumber(radiusValue)) || !dataModel) { return ''; } @@ -560,28 +477,29 @@ export abstract class RadialColumnSeriesBase< } getLegendData(legendType: _ModuleSupport.ChartLegendType): _ModuleSupport.CategoryLegendDatum[] { - const { id, data, angleKey, radiusKey, radiusName, visible } = this; - - if (!(data?.length && angleKey && radiusKey && legendType === 'category')) { + if (!this.data?.length || !this.properties.isValid() || legendType !== 'category') { return []; } + const { radiusKey, radiusName, fill, stroke, fillOpacity, strokeOpacity, strokeWidth, visible } = + this.properties; + return [ { legendType: 'category', - id, + id: this.id, itemId: radiusKey, - seriesId: id, + seriesId: this.id, enabled: visible, label: { text: radiusName ?? radiusKey, }, marker: { - fill: this.fill ?? 'rgba(0, 0, 0, 0)', - stroke: this.stroke ?? 'rgba(0, 0, 0, 0)', - fillOpacity: this.fillOpacity ?? 1, - strokeOpacity: this.strokeOpacity ?? 1, - strokeWidth: this.strokeWidth, + fill: fill ?? 'rgba(0, 0, 0, 0)', + stroke: stroke ?? 'rgba(0, 0, 0, 0)', + fillOpacity: fillOpacity ?? 1, + strokeOpacity: strokeOpacity ?? 1, + strokeWidth, }, }, ]; diff --git a/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeriesBaseProperties.ts b/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeriesBaseProperties.ts new file mode 100644 index 0000000000..dc7fb32713 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeriesBaseProperties.ts @@ -0,0 +1,77 @@ +import type { + AgBaseRadialColumnSeriesOptions, + AgRadialSeriesFormat, + AgRadialSeriesFormatterParams, + AgRadialSeriesLabelFormatterParams, + AgRadialSeriesTooltipRendererParams, +} from 'ag-charts-community'; +import { _ModuleSupport, _Scene } from 'ag-charts-community'; + +const { Label } = _Scene; +const { + SeriesProperties, + SeriesTooltip, + Validate, + COLOR_STRING, + DEGREE, + FUNCTION, + LINE_DASH, + NUMBER, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, +} = _ModuleSupport; + +export class RadialColumnSeriesBaseProperties extends SeriesProperties { + @Validate(STRING) + angleKey!: string; + + @Validate(STRING, { optional: true }) + angleName?: string; + + @Validate(STRING) + radiusKey!: string; + + @Validate(STRING, { optional: true }) + radiusName?: string; + + @Validate(COLOR_STRING) + fill: string = 'black'; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING) + stroke: string = 'black'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgRadialSeriesFormatterParams) => AgRadialSeriesFormat; + + @Validate(DEGREE) + rotation = 0; + + @Validate(STRING, { optional: true }) + stackGroup?: string; + + @Validate(NUMBER, { optional: true }) + normalizedTo?: number; + + @Validate(OBJECT) + readonly label = new Label(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeriesProperties.ts new file mode 100644 index 0000000000..66c508f574 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/radial-column/radialColumnSeriesProperties.ts @@ -0,0 +1,16 @@ +import type { AgBaseRadialColumnSeriesOptions } from 'ag-charts-community'; +import { _ModuleSupport } from 'ag-charts-community'; + +import { RadialColumnSeriesBaseProperties } from './radialColumnSeriesBaseProperties'; + +const { Validate, RATIO } = _ModuleSupport; + +export class RadialColumnSeriesProperties< + T extends AgBaseRadialColumnSeriesOptions, +> extends RadialColumnSeriesBaseProperties { + @Validate(RATIO, { optional: true }) + columnWidthRatio?: number; + + @Validate(RATIO, { optional: true }) + maxColumnWidthRatio?: number; +} diff --git a/packages/ag-charts-enterprise/src/series/range-area/rangeArea.ts b/packages/ag-charts-enterprise/src/series/range-area/rangeArea.ts index 63a71d7be7..f27109032f 100644 --- a/packages/ag-charts-enterprise/src/series/range-area/rangeArea.ts +++ b/packages/ag-charts-enterprise/src/series/range-area/rangeArea.ts @@ -1,23 +1,12 @@ -import type { - AgRangeAreaSeriesLabelFormatterParams, - AgRangeAreaSeriesLabelPlacement, - AgRangeAreaSeriesOptionsKeys, - AgRangeAreaSeriesTooltipRendererParams, -} from 'ag-charts-community'; import { _ModuleSupport, _Scene, _Util } from 'ag-charts-community'; +import { RangeAreaProperties } from './rangeAreaProperties'; + const { - Validate, valueProperty, trailingValueProperty, keyProperty, ChartAxisDirection, - RATIO, - PLACEMENT, - POSITIVE_NUMBER, - STRING, - COLOR_STRING, - LINE_DASH, mergeDefaults, updateLabelNode, fixNumericExtent, @@ -54,7 +43,8 @@ type RangeAreaLabelDatum = Readonly<_Scene.Point> & { series: _ModuleSupport.CartesianSeriesNodeDatum['series']; }; -interface RangeAreaMarkerDatum extends Required> { +export interface RangeAreaMarkerDatum + extends Required> { readonly index: number; readonly yLowKey: string; readonly yHighKey: string; @@ -77,20 +67,12 @@ class RangeAreaSeriesNodeClickEvent< constructor(type: TEvent, nativeEvent: MouseEvent, datum: RangeAreaMarkerDatum, series: RangeAreaSeries) { super(type, nativeEvent, datum, series); - this.xKey = series.xKey; - this.yLowKey = series.yLowKey; - this.yHighKey = series.yHighKey; + this.xKey = series.properties.xKey; + this.yLowKey = series.properties.yLowKey; + this.yHighKey = series.properties.yHighKey; } } -class RangeAreaSeriesLabel extends _Scene.Label { - @Validate(PLACEMENT) - placement: AgRangeAreaSeriesLabelPlacement = 'outside'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - padding: number = 6; -} - type RadarAreaPoint = _ModuleSupport.AreaPathPoint & { size: number }; type RadarAreaPathDatum = { @@ -107,35 +89,9 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< static className = 'RangeAreaSeries'; static type = 'range-area' as const; - protected override readonly NodeClickEvent = RangeAreaSeriesNodeClickEvent; - - readonly marker = new _ModuleSupport.SeriesMarker(); - readonly label = new RangeAreaSeriesLabel(); - - tooltip = new _ModuleSupport.SeriesTooltip(); - - shadow?: _Scene.DropShadow = undefined; - - @Validate(COLOR_STRING, { optional: true }) - fill: string = '#99CCFF'; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = '#99CCFF'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; + override properties = new RangeAreaProperties(); - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; + protected override readonly NodeClickEvent = RangeAreaSeriesNodeClickEvent; constructor(moduleCtx: _ModuleSupport.ModuleContext) { super({ @@ -153,32 +109,12 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< }); } - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yLowKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yLowName?: string = undefined; - - @Validate(STRING, { optional: true }) - yHighKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yHighName?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - override async processData(dataController: _ModuleSupport.DataController) { - const { xKey, yLowKey, yHighKey, data = [] } = this; - - if (!yLowKey || !yHighKey) return; + if (!this.properties.isValid()) { + return; + } + const { xKey, yLowKey, yHighKey } = this.properties; const { isContinuousX, isContinuousY } = this.isContinuous(); const extraProps = []; @@ -190,7 +126,7 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< extraProps.push(animationValidation(this)); } - await this.requestDataModel(dataController, data, { + await this.requestDataModel(dataController, this.data ?? [], { props: [ keyProperty(this, xKey, isContinuousX, { id: `xValue` }), valueProperty(this, yLowKey, isContinuousY, { id: `yLowValue`, invalidValue: undefined }), @@ -257,7 +193,7 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< const xScale = xAxis.scale; const yScale = yAxis.scale; - const { yLowKey = '', yHighKey = '', xKey = '', processedData } = this; + const { xKey, yLowKey, yHighKey, marker } = this.properties; const itemId = `${yLowKey}-${yHighKey}`; const xOffset = (xScale.bandwidth ?? 0) / 2; @@ -276,8 +212,8 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< const yLowCoordinate = yScale.convert(yLow); return [ - { point: { x, y: yHighCoordinate }, size: this.marker.size, itemId: `high`, yValue: yHigh, xValue }, - { point: { x, y: yLowCoordinate }, size: this.marker.size, itemId: `low`, yValue: yLow, xValue }, + { point: { x, y: yHighCoordinate }, size: marker.size, itemId: `high`, yValue: yHigh, xValue }, + { point: { x, y: yLowCoordinate }, size: marker.size, itemId: `low`, yValue: yLow, xValue }, ]; }; @@ -309,7 +245,7 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< let lastXValue: any; let lastYHighDatum: any = -Infinity; let lastYLowDatum: any = -Infinity; - processedData?.data.forEach(({ keys, datum, values }, datumIdx) => { + this.processedData?.data.forEach(({ keys, datum, values }, datumIdx) => { const dataValues = dataModel.resolveProcessedDataDefsValues(defs, { keys, values }); const { xValue, yHighValue, yLowValue } = dataValues; @@ -419,7 +355,8 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< datum: any; series: RangeAreaSeries; }): RangeAreaLabelDatum { - const { placement, padding = 10 } = this.label; + const { xKey, yLowKey, yHighKey, xName, yName, yLowName, yHighName, label } = this.properties; + const { placement, padding = 10 } = label; const actualItemId = inverted ? (itemId === 'low' ? 'high' : 'low') : itemId; const direction = @@ -434,19 +371,8 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< itemId, datum, text: this.getLabelText( - this.label, - { - value, - datum, - itemId, - xKey: this.xKey ?? '', - yLowKey: this.yLowKey ?? '', - yHighKey: this.yHighKey ?? '', - xName: this.xName, - yLowName: this.yLowName, - yHighName: this.yHighName, - yName: this.yName, - }, + label, + { value, datum, itemId, xKey, yLowKey, yHighKey, xName, yLowName, yHighName, yName }, (value) => (isNumber(value) ? value.toFixed(2) : String(value)) ), textAlign: 'center', @@ -455,11 +381,11 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< } protected override isPathOrSelectionDirty(): boolean { - return this.marker.isDirty(); + return this.properties.marker.isDirty(); } protected override markerFactory() { - const { shape } = this.marker; + const { shape } = this.properties.marker; const MarkerShape = getMarker(shape); return new MarkerShape(); } @@ -469,17 +395,17 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< const [fill, stroke] = opts.paths; const { seriesRectHeight: height, seriesRectWidth: width } = this.nodeDataDependencies; - const strokeWidth = this.getStrokeWidth(this.strokeWidth); + const strokeWidth = this.getStrokeWidth(this.properties.strokeWidth); stroke.setProperties({ tag: AreaSeriesTag.Stroke, fill: undefined, lineJoin: (stroke.lineCap = 'round'), pointerEvents: PointerEvents.None, - stroke: this.stroke, + stroke: this.properties.stroke, strokeWidth, - strokeOpacity: this.strokeOpacity, - lineDash: this.lineDash, - lineDashOffset: this.lineDashOffset, + strokeOpacity: this.properties.strokeOpacity, + lineDash: this.properties.lineDash, + lineDashOffset: this.properties.lineDashOffset, opacity, visible, }); @@ -488,12 +414,12 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< stroke: undefined, lineJoin: 'round', pointerEvents: PointerEvents.None, - fill: this.fill, - fillOpacity: this.fillOpacity, - lineDash: this.lineDash, - lineDashOffset: this.lineDashOffset, - strokeOpacity: this.strokeOpacity, - fillShadow: this.shadow, + fill: this.properties.fill, + fillOpacity: this.properties.fillOpacity, + lineDash: this.properties.lineDash, + lineDashOffset: this.properties.lineDashOffset, + strokeOpacity: this.properties.strokeOpacity, + fillShadow: this.properties.shadow, strokeWidth, opacity, visible, @@ -559,17 +485,11 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< markerSelection: _Scene.Selection<_Scene.Marker, RangeAreaMarkerDatum>; }) { const { nodeData, markerSelection } = opts; - const { - marker: { enabled }, - } = this; - const data = enabled && nodeData ? nodeData : []; - - if (this.marker.isDirty()) { + if (this.properties.marker.isDirty()) { markerSelection.clear(); markerSelection.cleanup(); } - - return markerSelection.update(data); + return markerSelection.update(this.properties.marker.enabled ? nodeData : []); } protected override async updateMarkerNodes(opts: { @@ -577,19 +497,10 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< isHighlight: boolean; }) { const { markerSelection, isHighlight: highlighted } = opts; - const { - xKey = '', - yLowKey = '', - yHighKey = '', - marker, - fill, - stroke, - strokeWidth, - fillOpacity, - strokeOpacity, - } = this; + const { xKey, yLowKey, yHighKey, marker, fill, stroke, strokeWidth, fillOpacity, strokeOpacity } = + this.properties; - const baseStyle = mergeDefaults(highlighted && this.highlightStyle.item, marker.getStyle(), { + const baseStyle = mergeDefaults(highlighted && this.properties.highlightStyle.item, marker.getStyle(), { fill, fillOpacity, stroke, @@ -602,7 +513,7 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< }); if (!highlighted) { - this.marker.markClean(); + this.properties.marker.markClean(); } } @@ -620,7 +531,7 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< protected async updateLabelNodes(opts: { labelSelection: _Scene.Selection<_Scene.Text, RangeAreaLabelDatum> }) { opts.labelSelection.each((textNode, datum) => { - updateLabelNode(textNode, this.label, datum); + updateLabelNode(textNode, this.properties.label, datum); }); } @@ -641,17 +552,15 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< } getTooltipHtml(nodeDatum: RangeAreaMarkerDatum): string { - const { xKey, yLowKey, yHighKey, axes } = this; - - const xAxis = axes[ChartAxisDirection.X]; - const yAxis = axes[ChartAxisDirection.Y]; + const xAxis = this.axes[ChartAxisDirection.X]; + const yAxis = this.axes[ChartAxisDirection.Y]; - if (!xKey || !yLowKey || !yHighKey || !xAxis || !yAxis) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { xName, yLowName, yHighName, yName, id: seriesId, fill, tooltip } = this; - + const { id: seriesId } = this; + const { xKey, yLowKey, yHighKey, xName, yName, yLowName, yHighName, fill, tooltip } = this.properties; const { datum, itemId, xValue, yLowValue, yHighValue } = nodeDatum; const color = fill ?? 'gray'; @@ -674,56 +583,45 @@ export class RangeAreaSeries extends _ModuleSupport.CartesianSeries< return tooltip.toTooltipHtml( { title, content, backgroundColor: color }, - { - seriesId, - itemId, - datum, - xKey, - yLowKey, - yHighKey, - xName, - yLowName, - yHighName, - yName, - color, - } + { seriesId, itemId, datum, xKey, yLowKey, yHighKey, xName, yLowName, yHighName, yName, color } ); } getLegendData(legendType: _ModuleSupport.ChartLegendType): _ModuleSupport.CategoryLegendDatum[] { - const { id, visible } = this; - if (legendType !== 'category') { return []; } - const { fill, stroke, strokeWidth, fillOpacity, strokeOpacity, yName, yLowName, yHighName, yLowKey, yHighKey } = - this; + const { + yLowKey, + yHighKey, + yName, + yLowName, + yHighName, + fill, + stroke, + strokeWidth, + fillOpacity, + strokeOpacity, + visible, + } = this.properties; const legendItemText = yName ?? `${yLowName ?? yLowKey} - ${yHighName ?? yHighKey}`; return [ { legendType: 'category', - id, + id: this.id, itemId: `${yLowKey}-${yHighKey}`, - seriesId: id, + seriesId: this.id, enabled: visible, - label: { - text: `${legendItemText}`, - }, - marker: { - fill, - stroke, - fillOpacity, - strokeOpacity, - strokeWidth, - }, + label: { text: `${legendItemText}` }, + marker: { fill, stroke, fillOpacity, strokeOpacity, strokeWidth }, }, ]; } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } override onDataChange() {} diff --git a/packages/ag-charts-enterprise/src/series/range-area/rangeAreaProperties.ts b/packages/ag-charts-enterprise/src/series/range-area/rangeAreaProperties.ts new file mode 100644 index 0000000000..72dea3071b --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/range-area/rangeAreaProperties.ts @@ -0,0 +1,89 @@ +import type { + AgRangeAreaSeriesLabelFormatterParams, + AgRangeAreaSeriesLabelPlacement, + AgRangeAreaSeriesOptions, + AgRangeAreaSeriesOptionsKeys, + AgRangeAreaSeriesTooltipRendererParams, +} from 'ag-charts-community'; +import { _ModuleSupport, _Scene } from 'ag-charts-community'; + +import type { RangeAreaMarkerDatum } from './rangeArea'; + +const { DropShadow, Label } = _Scene; +const { + CartesianSeriesProperties, + SeriesMarker, + SeriesTooltip, + Validate, + COLOR_STRING, + LINE_DASH, + OBJECT, + PLACEMENT, + POSITIVE_NUMBER, + RATIO, + STRING, +} = _ModuleSupport; + +class RangeAreaSeriesLabel extends Label { + @Validate(PLACEMENT) + placement: AgRangeAreaSeriesLabelPlacement = 'outside'; + + @Validate(POSITIVE_NUMBER) + padding: number = 6; +} + +export class RangeAreaProperties extends CartesianSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yLowKey!: string; + + @Validate(STRING) + yHighKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + yLowName?: string; + + @Validate(STRING, { optional: true }) + yHighName?: string; + + @Validate(COLOR_STRING) + fill: string = '#99CCFF'; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING) + stroke: string = '#99CCFF'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(OBJECT) + readonly shadow = new DropShadow().set({ enabled: false }); + + @Validate(OBJECT) + readonly marker = new SeriesMarker(); + + @Validate(OBJECT) + readonly label = new RangeAreaSeriesLabel(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/range-bar/rangeBar.ts b/packages/ag-charts-enterprise/src/series/range-bar/rangeBar.ts index 2f62f88048..4e81d1eacf 100644 --- a/packages/ag-charts-enterprise/src/series/range-bar/rangeBar.ts +++ b/packages/ag-charts-enterprise/src/series/range-bar/rangeBar.ts @@ -1,26 +1,13 @@ -import type { - AgRangeBarSeriesFormat, - AgRangeBarSeriesFormatterParams, - AgRangeBarSeriesLabelFormatterParams, - AgRangeBarSeriesLabelPlacement, - AgRangeBarSeriesTooltipRendererParams, - AgTooltipRendererResult, -} from 'ag-charts-community'; +import type { AgTooltipRendererResult } from 'ag-charts-community'; import { _ModuleSupport, _Scene, _Util } from 'ag-charts-community'; +import { RangeBarProperties } from './rangeBarProperties'; + const { - Validate, SeriesNodePickMode, valueProperty, keyProperty, ChartAxisDirection, - PLACEMENT, - POSITIVE_NUMBER, - RATIO, - STRING, - FUNCTION, - COLOR_STRING, - LINE_DASH, getRectConfig, updateRect, checkCrisp, @@ -98,20 +85,12 @@ class RangeBarSeriesNodeClickEvent< constructor(type: TEvent, nativeEvent: MouseEvent, datum: RangeBarNodeDatum, series: RangeBarSeries) { super(type, nativeEvent, datum, series); - this.xKey = series.xKey; - this.yLowKey = series.yLowKey; - this.yHighKey = series.yHighKey; + this.xKey = series.properties.xKey; + this.yLowKey = series.properties.yLowKey; + this.yHighKey = series.properties.yHighKey; } } -class RangeBarSeriesLabel extends _Scene.Label { - @Validate(PLACEMENT) - placement: AgRangeBarSeriesLabelPlacement = 'inside'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - padding: number = 6; -} - export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< _Scene.Rect, RangeBarNodeDatum, @@ -120,40 +99,16 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< static className = 'RangeBarSeries'; static type = 'range-bar' as const; - protected override readonly NodeClickEvent = RangeBarSeriesNodeClickEvent; - - readonly label = new RangeBarSeriesLabel(); - - tooltip = new _ModuleSupport.SeriesTooltip(); - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgRangeBarSeriesFormatterParams) => AgRangeBarSeriesFormat = undefined; - - shadow?: _Scene.DropShadow = undefined; - - @Validate(COLOR_STRING, { optional: true }) - fill: string = '#99CCFF'; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = '#99CCFF'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; + override properties = new RangeBarProperties(); - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; + protected override readonly NodeClickEvent = RangeBarSeriesNodeClickEvent; - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; + /** + * Used to get the position of bars within each group. + */ + private groupScale = new BandScale(); - @Validate(POSITIVE_NUMBER) - cornerRadius: number = 0; + protected smallestDataInterval?: { x: number; y: number } = undefined; constructor(moduleCtx: _ModuleSupport.ModuleContext) { super({ @@ -170,11 +125,6 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< }); } - /** - * Used to get the position of bars within each group. - */ - private groupScale = new BandScale(); - protected override resolveKeyDirection(direction: _ModuleSupport.ChartAxisDirection) { if (this.getBarDirection() === ChartAxisDirection.X) { if (direction === ChartAxisDirection.X) { @@ -185,47 +135,24 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< return direction; } - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yLowKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yLowName?: string = undefined; - - @Validate(STRING, { optional: true }) - yHighKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yHighName?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - protected smallestDataInterval?: { x: number; y: number } = undefined; - override async processData(dataController: _ModuleSupport.DataController) { - const { xKey, yLowKey, yHighKey, data = [] } = this; - - if (!yLowKey || !yHighKey) return; + if (!this.properties.isValid()) { + return; + } + const { xKey, yLowKey, yHighKey } = this.properties; const isContinuousX = ContinuousScale.is(this.getCategoryAxis()?.scale); const isContinuousY = ContinuousScale.is(this.getValueAxis()?.scale); const extraProps = []; - const animationEnabled = !this.ctx.animationManager.isSkipped(); - if (!this.ctx.animationManager.isSkipped() && this.processedData) { - extraProps.push(diff(this.processedData)); - } - if (animationEnabled) { + if (!this.ctx.animationManager.isSkipped()) { + if (this.processedData) { + extraProps.push(diff(this.processedData)); + } extraProps.push(animationValidation(this)); } - const { processedData } = await this.requestDataModel(dataController, data, { + const { processedData } = await this.requestDataModel(dataController, this.data ?? [], { props: [ keyProperty(this, xKey, isContinuousX, { id: 'xValue' }), valueProperty(this, yLowKey, isContinuousY, { id: `yLowValue` }), @@ -289,13 +216,11 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< const { data, dataModel, - smallestDataInterval, - visible, groupScale, - fill, - stroke, - strokeWidth, + processedData, + smallestDataInterval, ctx: { seriesStateManager }, + properties: { visible }, } = this; const xAxis = this.getCategoryAxis(); const yAxis = this.getValueAxis(); @@ -308,8 +233,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< const yScale = yAxis.scale; const barAlongX = this.getBarDirection() === ChartAxisDirection.X; - - const { yLowKey = '', yHighKey = '', xKey = '', processedData } = this; + const { xKey, yLowKey, yHighKey, fill, stroke, strokeWidth } = this.properties; const itemId = `${yLowKey}-${yHighKey}`; @@ -440,27 +364,19 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< datum: any; series: RangeBarSeries; }): RangeBarNodeLabelDatum[] { - const { placement, padding } = this.label; + const { xKey, yLowKey, yHighKey, xName, yLowName, yHighName, yName, label } = this.properties; + const labelParams = { datum, xKey, yLowKey, yHighKey, xName, yLowName, yHighName, yName }; + const { placement, padding } = label; const paddingDirection = placement === 'outside' ? 1 : -1; const labelPadding = padding * paddingDirection; - const labelParams = { - datum, - xKey: this.xKey ?? '', - yLowKey: this.yLowKey ?? '', - yHighKey: this.yHighKey ?? '', - xName: this.xName, - yLowName: this.yLowName, - yHighName: this.yHighName, - yName: this.yName, - }; const yLowLabel: RangeBarNodeLabelDatum = { x: rect.x + (barAlongX ? -labelPadding : rect.width / 2), y: rect.y + (barAlongX ? rect.height / 2 : rect.height + labelPadding), textAlign: barAlongX ? 'left' : 'center', textBaseline: barAlongX ? 'middle' : 'bottom', - text: this.getLabelText(this.label, { itemId: 'low', value: yLowValue, ...labelParams }, (value) => + text: this.getLabelText(label, { itemId: 'low', value: yLowValue, ...labelParams }, (value) => isNumber(value) ? value.toFixed(2) : '' ), itemId: 'low', @@ -472,7 +388,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< y: rect.y + (barAlongX ? rect.height / 2 : -labelPadding), textAlign: barAlongX ? 'right' : 'center', textBaseline: barAlongX ? 'middle' : 'top', - text: this.getLabelText(this.label, { itemId: 'high', value: yHighValue, ...labelParams }, (value) => + text: this.getLabelText(label, { itemId: 'high', value: yHighValue, ...labelParams }, (value) => isNumber(value) ? value.toFixed(2) : '' ), itemId: 'high', @@ -508,13 +424,12 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< isHighlight: boolean; }) { const { datumSelection, isHighlight } = opts; + const { id: seriesId, ctx } = this; const { - yLowKey = '', - yHighKey = '', + yLowKey, + yHighKey, highlightStyle: { item: itemHighlightStyle }, - id: seriesId, - ctx, - } = this; + } = this.properties; const xAxis = this.axes[ChartAxisDirection.X]; const crisp = checkCrisp(xAxis?.visibleRange); @@ -530,7 +445,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< lineDashOffset, formatter, shadow: fillShadow, - } = this; + } = this.properties; const style: _ModuleSupport.RectConfig = { fill: datum.fill, stroke: datum.stroke, @@ -540,7 +455,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< lineDashOffset, fillShadow, strokeWidth: this.getStrokeWidth(strokeWidth), - cornerRadius: this.cornerRadius, + cornerRadius: this.properties.cornerRadius, cornerRadiusBbox: undefined, }; const visible = categoryAlongX ? datum.width > 0 : datum.height > 0; @@ -577,7 +492,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< labelData: RangeBarNodeLabelDatum[]; labelSelection: RangeBarAnimationData['labelSelections'][number]; }) { - const labelData = this.label.enabled ? opts.labelData : []; + const labelData = this.properties.label.enabled ? opts.labelData : []; return opts.labelSelection.update(labelData, (text) => { text.pointerEvents = PointerEvents.None; }); @@ -585,30 +500,28 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< protected async updateLabelNodes(opts: { labelSelection: _Scene.Selection<_Scene.Text, any> }) { opts.labelSelection.each((textNode, datum) => { - updateLabelNode(textNode, this.label, datum); + updateLabelNode(textNode, this.properties.label, datum); }); } getTooltipHtml(nodeDatum: RangeBarNodeDatum): string { const { - xKey, - yLowKey, - yHighKey, + id: seriesId, ctx: { callbackCache }, } = this; const xAxis = this.getCategoryAxis(); const yAxis = this.getValueAxis(); - if (!xKey || !yLowKey || !yHighKey || !xAxis || !yAxis) { + if (!this.properties.isValid() || !xAxis || !yAxis) { return ''; } - const { xName, yLowName, yHighName, yName, id: seriesId, fill, strokeWidth, formatter, tooltip } = this; + const { xKey, yLowKey, yHighKey, xName, yLowName, yHighName, yName, fill, strokeWidth, formatter, tooltip } = + this.properties; const { datum, itemId, xValue, yLowValue, yHighValue } = nodeDatum; let format; - if (formatter) { format = callbackCache.call(formatter, { datum, @@ -670,7 +583,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< } const { fill, stroke, strokeWidth, fillOpacity, strokeOpacity, yName, yLowName, yHighName, yLowKey, yHighKey } = - this; + this.properties; const legendItemText = yName ?? `${yLowName ?? yLowKey} - ${yHighName ?? yHighKey}`; return [ @@ -687,7 +600,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< } override animateEmptyUpdateReady({ datumSelections, labelSelections }: RangeBarAnimationData) { - const fns = prepareBarAnimationFunctions(midpointStartingBarPosition(this.direction === 'vertical')); + const fns = prepareBarAnimationFunctions(midpointStartingBarPosition(this.isVertical())); motion.fromToMotion(this.id, 'datums', this.ctx.animationManager, datumSelections, fns); seriesLabelFadeInAnimation(this, 'labels', this.ctx.animationManager, labelSelections); } @@ -699,7 +612,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< this.ctx.animationManager.stopByAnimationGroupId(this.id); - const fns = prepareBarAnimationFunctions(midpointStartingBarPosition(this.direction === 'vertical')); + const fns = prepareBarAnimationFunctions(midpointStartingBarPosition(this.isVertical())); motion.fromToMotion( this.id, 'datums', @@ -718,7 +631,7 @@ export class RangeBarSeries extends _ModuleSupport.AbstractBarSeries< } protected isLabelEnabled() { - return this.label.enabled; + return this.properties.label.enabled; } protected override onDataChange() {} diff --git a/packages/ag-charts-enterprise/src/series/range-bar/rangeBarProperties.ts b/packages/ag-charts-enterprise/src/series/range-bar/rangeBarProperties.ts new file mode 100644 index 0000000000..0775e622be --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/range-bar/rangeBarProperties.ts @@ -0,0 +1,91 @@ +import type { + AgRangeBarSeriesFormat, + AgRangeBarSeriesFormatterParams, + AgRangeBarSeriesLabelFormatterParams, + AgRangeBarSeriesLabelPlacement, + AgRangeBarSeriesOptions, + AgRangeBarSeriesTooltipRendererParams, +} from 'ag-charts-community'; +import { _ModuleSupport, _Scene } from 'ag-charts-community'; + +const { DropShadow, Label } = _Scene; +const { + AbstractBarSeriesProperties, + SeriesTooltip, + Validate, + COLOR_STRING, + FUNCTION, + LINE_DASH, + OBJECT, + PLACEMENT, + POSITIVE_NUMBER, + RATIO, + STRING, +} = _ModuleSupport; + +class RangeBarSeriesLabel extends Label { + @Validate(PLACEMENT) + placement: AgRangeBarSeriesLabelPlacement = 'inside'; + + @Validate(POSITIVE_NUMBER) + padding: number = 6; +} + +export class RangeBarProperties extends AbstractBarSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yLowKey!: string; + + @Validate(STRING) + yHighKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(STRING, { optional: true }) + yLowName?: string; + + @Validate(STRING, { optional: true }) + yHighName?: string; + + @Validate(COLOR_STRING) + fill: string = '#99CCFF'; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING) + stroke: string = '#99CCFF'; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(POSITIVE_NUMBER) + cornerRadius: number = 0; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgRangeBarSeriesFormatterParams) => AgRangeBarSeriesFormat; + + @Validate(OBJECT) + readonly shadow = new DropShadow().set({ enabled: false }); + + @Validate(OBJECT) + readonly label = new RangeBarSeriesLabel(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +} diff --git a/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.test.ts b/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.test.ts index d98e65a552..3cee221cd3 100644 --- a/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.test.ts +++ b/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.test.ts @@ -313,7 +313,7 @@ describe('SunburstChart', () => { }, getDatumValues: (item, series) => { const { datum } = item.datum; - return [datum[series.labelKey], datum[series.sizeKey]]; + return [datum[series.properties.labelKey], datum[series.properties.sizeKey]]; }, getTooltipRenderedValues: (params) => { const { datum } = params; diff --git a/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.ts b/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.ts index 9327de395a..209cd6f29b 100644 --- a/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.ts +++ b/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeries.ts @@ -1,28 +1,16 @@ import { - type AgSunburstSeriesFormatterParams, type AgSunburstSeriesLabelFormatterParams, type AgSunburstSeriesStyle, - type AgSunburstSeriesTooltipRendererParams, type AgTooltipRendererResult, _ModuleSupport, _Scene, _Util, } from 'ag-charts-community'; -import { AutoSizeableSecondaryLabel, AutoSizedLabel, formatLabels } from '../util/labelFormatter'; - -const { - fromToMotion, - HighlightStyle, - COLOR_STRING, - FUNCTION, - NUMBER, - POSITIVE_NUMBER, - STRING, - RATIO, - SeriesTooltip, - Validate, -} = _ModuleSupport; +import { formatLabels } from '../util/labelFormatter'; +import { SunburstSeriesProperties } from './sunburstSeriesProperties'; + +const { fromToMotion } = _ModuleSupport; const { Sector, Group, Selection, Text } = _Scene; const { sanitizeHtml } = _Util; @@ -48,27 +36,6 @@ const getAngleData = ( return angleData; }; -class SunburstSeriesTileHighlightStyle extends HighlightStyle { - readonly label = new AutoSizedLabel(); - - readonly secondaryLabel = new AutoSizedLabel(); - - @Validate(STRING, { optional: true }) - fill?: string = undefined; - - @Validate(RATIO, { optional: true }) - fillOpacity?: number = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth?: number = undefined; - - @Validate(RATIO, { optional: true }) - strokeOpacity?: number = undefined; -} - enum CircleQuarter { TopLeft = 0b0001, TopRight = 0b0010, @@ -91,13 +58,11 @@ enum TextNodeTag { Secondary, } -export class SunburstSeries< - TDatum extends _ModuleSupport.SeriesNodeDatum = _ModuleSupport.SeriesNodeDatum, -> extends _ModuleSupport.HierarchySeries<_Scene.Group, TDatum> { +export class SunburstSeries extends _ModuleSupport.HierarchySeries<_Scene.Group, _ModuleSupport.SeriesNodeDatum> { static className = 'SunburstSeries'; static type = 'sunburst' as const; - readonly tooltip = new SeriesTooltip>(); + override properties = new SunburstSeriesProperties(); groupSelection = Selection.select(this.contentGroup, Group); private highlightSelection: _Scene.Selection<_Scene.Group, _ModuleSupport.HierarchyNode> = Selection.select( @@ -109,41 +74,8 @@ export class SunburstSeries< private labelData?: (LabelData | undefined)[]; - override readonly highlightStyle = new SunburstSeriesTileHighlightStyle(); - - readonly label = new AutoSizedLabel>(); - - readonly secondaryLabel = new AutoSizeableSecondaryLabel>(); - - @Validate(STRING, { optional: true }) - sizeName?: string = undefined; - - @Validate(STRING, { optional: true }) - labelKey?: string = undefined; - - @Validate(STRING, { optional: true }) - secondaryLabelKey?: string = undefined; - - @Validate(RATIO) - fillOpacity: number = 1; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 0; - - @Validate(RATIO) - strokeOpacity: number = 1; - - @Validate(NUMBER, { optional: true }) - sectorSpacing?: number = undefined; - - @Validate(NUMBER, { optional: true }) - padding?: number = undefined; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgSunburstSeriesFormatterParams) => AgSunburstSeriesStyle = undefined; - override async processData() { - const { childrenKey, colorKey, colorName, labelKey, secondaryLabelKey, sizeKey, sizeName } = this; + const { childrenKey, colorKey, colorName, labelKey, secondaryLabelKey, sizeKey, sizeName } = this.properties; super.processData(); @@ -165,7 +97,7 @@ export class SunburstSeries< if (datum != null && depth != null && labelKey != null) { const value = (datum as any)[labelKey]; label = this.getLabelText( - this.label, + this.properties.label, { depth, datum, @@ -189,7 +121,7 @@ export class SunburstSeries< if (datum != null && depth != null && secondaryLabelKey != null) { const value = (datum as any)[secondaryLabelKey]; secondaryLabel = this.getLabelText( - this.secondaryLabel, + this.properties.secondaryLabel, { depth, datum, @@ -238,11 +170,14 @@ export class SunburstSeries< } async updateNodes() { - const { chart, data, maxDepth, sectorSpacing = 0, padding = 0, highlightStyle, labelData } = this; + const { chart, data, maxDepth, labelData } = this; - if (chart == null || data == null || labelData == null) return; + if (chart == null || data == null || labelData == null) { + return; + } const { width, height } = chart.seriesRect!; + const { sectorSpacing = 0, padding = 0, highlightStyle } = this.properties; this.contentGroup.translationX = width / 2; this.contentGroup.translationY = height / 2; @@ -258,7 +193,7 @@ export class SunburstSeries< this.ctx.highlightManager?.getActiveHighlight() as any; const labelTextNode = new Text(); - labelTextNode.setFont(this.label); + labelTextNode.setFont(this.properties.label); this.rootNode.walk((node) => { const angleDatum = this.angleData[node.index]; @@ -296,10 +231,10 @@ export class SunburstSeries< const format = this.getSectorFormat(node, highlighted); const fill = format?.fill ?? highlightedFill ?? node.fill; - const fillOpacity = format?.fillOpacity ?? highlightedFillOpacity ?? this.fillOpacity; + const fillOpacity = format?.fillOpacity ?? highlightedFillOpacity ?? this.properties.fillOpacity; const stroke = format?.stroke ?? highlightedStroke ?? node.stroke; - const strokeWidth = format?.strokeWidth ?? highlightedStrokeWidth ?? this.strokeWidth; - const strokeOpacity = format?.strokeOpacity ?? highlightedStrokeOpacity ?? this.strokeOpacity; + const strokeWidth = format?.strokeWidth ?? highlightedStrokeWidth ?? this.properties.strokeWidth; + const strokeOpacity = format?.strokeOpacity ?? highlightedStrokeOpacity ?? this.properties.strokeOpacity; sector.fill = fill; sector.fillOpacity = fillOpacity; @@ -384,9 +319,9 @@ export class SunburstSeries< const formatting = formatLabels( labelDatum?.label, - this.label, + this.properties.label, labelDatum?.secondaryLabel, - this.secondaryLabel, + this.properties.secondaryLabel, { padding }, sizeFittingHeight ); @@ -435,9 +370,9 @@ export class SunburstSeries< ) => { const { index, depth } = node; const meta = labelMeta?.[index]; - const labelStyle = tag === TextNodeTag.Primary ? this.label : this.secondaryLabel; + const labelStyle = tag === TextNodeTag.Primary ? this.properties.label : this.properties.secondaryLabel; const label = tag === TextNodeTag.Primary ? meta?.label : meta?.secondaryLabel; - if (depth == null || meta == null || label == null || meta == null) { + if (depth == null || meta == null || label == null) { text.visible = false; return; } @@ -447,7 +382,9 @@ export class SunburstSeries< let highlightedColor: string | undefined; if (highlighted) { const highlightedLabelStyle = - tag === TextNodeTag.Primary ? this.highlightStyle.label : this.highlightStyle.secondaryLabel; + tag === TextNodeTag.Primary + ? this.properties.highlightStyle.label + : this.properties.highlightStyle.secondaryLabel; highlightedColor = highlightedLabelStyle.color; } @@ -512,14 +449,15 @@ export class SunburstSeries< private getSectorFormat(node: _ModuleSupport.HierarchyNode, isHighlighted: boolean): AgSunburstSeriesStyle { const { datum, fill, stroke, depth } = node; const { - formatter, ctx: { callbackCache }, + properties: { formatter }, } = this; + if (!formatter || datum == null || depth == null) { return {}; } - const { colorKey, labelKey, sizeKey, strokeWidth } = this; + const { colorKey, labelKey, sizeKey, strokeWidth } = this.properties; const result = callbackCache.call(formatter, { seriesId: this.id, @@ -538,6 +476,7 @@ export class SunburstSeries< } override getTooltipHtml(node: _ModuleSupport.HierarchyNode): string { + const { id: seriesId } = this; const { tooltip, colorKey, @@ -546,8 +485,7 @@ export class SunburstSeries< secondaryLabelKey, sizeKey, sizeName = sizeKey, - id: seriesId, - } = this; + } = this.properties; const { datum, depth } = node; if (datum == null || depth == null) { return ''; @@ -558,7 +496,7 @@ export class SunburstSeries< const format = this.getSectorFormat(node, false); const color = format?.fill ?? node.fill; - if (!tooltip.renderer && !tooltip.format && !title) { + if (!tooltip.renderer && !title) { return ''; } @@ -583,7 +521,7 @@ export class SunburstSeries< const defaults: AgTooltipRendererResult = { title, - color: this.label.color, + color: this.properties.label.color, backgroundColor: color, content, }; @@ -607,24 +545,22 @@ export class SunburstSeries< protected override animateEmptyUpdateReady({ datumSelections, - }: _ModuleSupport.HierarchyAnimationData<_Scene.Group, TDatum>) { - fromToMotion<_Scene.Group, Pick<_Scene.Group, 'scalingX' | 'scalingY'>, _ModuleSupport.HierarchyNode>( - this.id, - 'nodes', - this.ctx.animationManager, - datumSelections, - { - toFn(_group, _datum, _status) { + }: _ModuleSupport.HierarchyAnimationData<_Scene.Group, _ModuleSupport.SeriesNodeDatum>) { + fromToMotion< + _Scene.Group, + Pick<_Scene.Group, 'scalingX' | 'scalingY'>, + _ModuleSupport.HierarchyNode<_ModuleSupport.SeriesNodeDatum> + >(this.id, 'nodes', this.ctx.animationManager, datumSelections, { + toFn(_group, _datum, _status) { + return { scalingX: 1, scalingY: 1 }; + }, + fromFn(group, datum, status) { + if (status === 'unknown' && datum != null && group.previousDatum == null) { + return { scalingX: 0, scalingY: 0 }; + } else { return { scalingX: 1, scalingY: 1 }; - }, - fromFn(group, datum, status) { - if (status === 'unknown' && datum != null && group.previousDatum == null) { - return { scalingX: 0, scalingY: 0 }; - } else { - return { scalingX: 1, scalingY: 1 }; - } - }, - } - ); + } + }, + }); } } diff --git a/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeriesProperties.ts new file mode 100644 index 0000000000..e5518e5660 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/sunburst/sunburstSeriesProperties.ts @@ -0,0 +1,88 @@ +import type { + AgSunburstSeriesFormatterParams, + AgSunburstSeriesLabelFormatterParams, + AgSunburstSeriesOptions, + AgSunburstSeriesStyle, + AgSunburstSeriesTooltipRendererParams, +} from 'ag-charts-community'; +import { _ModuleSupport } from 'ag-charts-community'; + +import { AutoSizeableSecondaryLabel, AutoSizedLabel } from '../util/labelFormatter'; + +const { + HierarchySeriesProperties, + HighlightStyle, + SeriesTooltip, + Validate, + COLOR_STRING, + FUNCTION, + NUMBER, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, +} = _ModuleSupport; + +class SunburstSeriesTileHighlightStyle extends HighlightStyle { + @Validate(STRING, { optional: true }) + fill?: string; + + @Validate(RATIO, { optional: true }) + fillOpacity?: number; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number; + + @Validate(RATIO, { optional: true }) + strokeOpacity?: number; + + @Validate(OBJECT) + readonly label = new AutoSizedLabel(); + + @Validate(OBJECT) + readonly secondaryLabel = new AutoSizedLabel(); +} + +export class SunburstSeriesProperties extends HierarchySeriesProperties { + @Validate(STRING, { optional: true }) + sizeName?: string; + + @Validate(STRING, { optional: true }) + labelKey?: string; + + @Validate(STRING, { optional: true }) + secondaryLabelKey?: string; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 0; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(NUMBER, { optional: true }) + sectorSpacing?: number; + + @Validate(NUMBER, { optional: true }) + padding?: number; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgSunburstSeriesFormatterParams) => AgSunburstSeriesStyle; + + @Validate(OBJECT) + override highlightStyle = new SunburstSeriesTileHighlightStyle(); + + @Validate(OBJECT) + readonly label = new AutoSizedLabel(); + + @Validate(OBJECT) + readonly secondaryLabel = new AutoSizeableSecondaryLabel(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip>(); +} diff --git a/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.test.ts b/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.test.ts index 6f0653f83f..94cb989dcf 100644 --- a/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.test.ts +++ b/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.test.ts @@ -310,7 +310,7 @@ describe('HierarchyChart', () => { getNodePoint: (item) => [item.x + item.width / 2, item.y + item.height / 2], getDatumValues: (item, series) => { const { datum } = item.datum; - return [datum[series.labelKey], datum[series.sizeKey]]; + return [datum[series.properties.labelKey], datum[series.properties.sizeKey]]; }, getTooltipRenderedValues: (params) => { const { datum } = params; diff --git a/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.ts b/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.ts index 9dd873ac61..858dbabe0e 100644 --- a/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.ts +++ b/packages/ag-charts-enterprise/src/series/treemap/treemapSeries.ts @@ -1,9 +1,6 @@ import { type AgTooltipRendererResult, - type AgTreemapSeriesFormatterParams, - type AgTreemapSeriesLabelFormatterParams, type AgTreemapSeriesStyle, - type AgTreemapSeriesTooltipRendererParams, type FontOptions, type TextAlign, type VerticalAlign, @@ -12,23 +9,10 @@ import { _Util, } from 'ag-charts-community'; -import { AutoSizeableSecondaryLabel, AutoSizedLabel, formatLabels } from '../util/labelFormatter'; - -const { - BOOLEAN, - HighlightStyle, - NUMBER, - POSITIVE_NUMBER, - COLOR_STRING, - FUNCTION, - STRING, - RATIO, - SeriesTooltip, - TEXT_ALIGN, - Validate, - VERTICAL_ALIGN, -} = _ModuleSupport; -const { Rect, Label, Group, BBox, Selection, Text } = _Scene; +import { AutoSizedLabel, formatLabels } from '../util/labelFormatter'; +import { TreemapSeriesProperties } from './treemapSeriesProperties'; + +const { Rect, Group, BBox, Selection, Text } = _Scene; const { Color, Logger, isEqual, sanitizeHtml } = _Util; type Side = 'left' | 'right' | 'top' | 'bottom'; @@ -38,121 +22,6 @@ interface LabelData { secondaryLabel: string | undefined; } -class TreemapGroupLabel extends Label { - @Validate(NUMBER) - spacing: number = 0; -} - -class TreemapSeriesGroup { - readonly label = new TreemapGroupLabel(); - - @Validate(NUMBER, { optional: true }) - gap: number = 0; - - @Validate(BOOLEAN) - interactive: boolean = true; - - @Validate(TEXT_ALIGN) - textAlign: TextAlign = 'center'; - - @Validate(STRING, { optional: true }) - fill?: string = undefined; - - @Validate(RATIO, { optional: true }) - fillOpacity: number = 1; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth: number = 1; - - @Validate(RATIO, { optional: true }) - strokeOpacity: number = 1; - - @Validate(POSITIVE_NUMBER, { optional: true }) - padding: number = 0; -} - -class TreemapSeriesTile { - readonly label = new AutoSizedLabel(); - - readonly secondaryLabel = new AutoSizeableSecondaryLabel(); - - @Validate(NUMBER, { optional: true }) - gap: number = 0; - - @Validate(STRING, { optional: true }) - fill?: string = undefined; - - @Validate(RATIO, { optional: true }) - fillOpacity: number = 1; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth: number = 1; - - @Validate(RATIO, { optional: true }) - strokeOpacity: number = 1; - - @Validate(POSITIVE_NUMBER, { optional: true }) - padding: number = 0; - - @Validate(TEXT_ALIGN) - textAlign: TextAlign = 'center'; - - @Validate(VERTICAL_ALIGN) - verticalAlign: VerticalAlign = 'middle'; -} - -class TreemapSeriesGroupHighlightStyle { - readonly label = new AutoSizedLabel(); - - @Validate(STRING, { optional: true }) - fill?: string = undefined; - - @Validate(RATIO, { optional: true }) - fillOpacity?: number = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth?: number = undefined; - - @Validate(RATIO, { optional: true }) - strokeOpacity?: number = undefined; -} - -class TreemapSeriesTileHighlightStyle { - readonly label = new AutoSizedLabel(); - - readonly secondaryLabel = new AutoSizeableSecondaryLabel(); - - @Validate(STRING, { optional: true }) - fill?: string = undefined; - - @Validate(RATIO, { optional: true }) - fillOpacity?: number = undefined; - - @Validate(COLOR_STRING, { optional: true }) - stroke?: string = undefined; - - @Validate(POSITIVE_NUMBER, { optional: true }) - strokeWidth?: number = undefined; - - @Validate(RATIO, { optional: true }) - strokeOpacity?: number = undefined; -} - -class TreemapSeriesHighlightStyle extends HighlightStyle { - readonly group = new TreemapSeriesGroupHighlightStyle(); - - readonly tile = new TreemapSeriesTileHighlightStyle(); -} - enum TextNodeTag { Primary, Secondary, @@ -209,6 +78,8 @@ export class TreemapSeries< static className = 'TreemapSeries'; static type = 'treemap' as const; + override properties = new TreemapSeriesProperties(); + groupSelection = Selection.select(this.contentGroup, Group); private highlightSelection: _Scene.Selection<_Scene.Group, _ModuleSupport.HierarchyNode> = Selection.select( this.highlightGroup, @@ -217,36 +88,10 @@ export class TreemapSeries< private labelData?: (LabelData | undefined)[]; - readonly group = new TreemapSeriesGroup(); - - readonly tile = new TreemapSeriesTile(); - - override readonly highlightStyle = new TreemapSeriesHighlightStyle(); - - readonly tooltip = new SeriesTooltip>(); - - @Validate(STRING, { optional: true }) - sizeName?: string = undefined; - - @Validate(STRING, { optional: true }) - labelKey?: string = undefined; - - @Validate(STRING, { optional: true }) - secondaryLabelKey?: string = undefined; - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgTreemapSeriesFormatterParams) => AgTreemapSeriesStyle = undefined; - - // We haven't decided how to expose this yet, but we need to have this property so it can change between light and dark themes - undocumentedGroupFills: string[] = []; - - // We haven't decided how to expose this yet, but we need to have this property so it can change between light and dark themes - undocumentedGroupStrokes: string[] = []; - private groupTitleHeight(node: _ModuleSupport.HierarchyNode, bbox: _Scene.BBox): number | undefined { const label = this.labelData?.[node.index]?.label; - const { label: font } = this.group; + const { label: font } = this.properties.group; const heightRatioThreshold = 3; @@ -272,7 +117,7 @@ export class TreemapSeries< left: 0, }; } else if (node.children.length === 0) { - const { padding } = this.tile; + const { padding } = this.properties.tile; return { top: padding, right: padding, @@ -284,7 +129,7 @@ export class TreemapSeries< const { label: { spacing }, padding, - } = this.group; + } = this.properties.group; const fontHeight = this.groupTitleHeight(node, bbox); const titleHeight = fontHeight != null ? fontHeight + spacing : 0; @@ -297,12 +142,12 @@ export class TreemapSeries< } override async processData() { - super.processData(); + await super.processData(); - const { data, childrenKey, colorKey, colorName, labelKey, secondaryLabelKey, sizeKey, sizeName, tile, group } = - this; + const { childrenKey, colorKey, colorName, labelKey, secondaryLabelKey, sizeKey, sizeName, tile, group } = + this.properties; - if (data == null || data.length === 0) { + if (!this.data?.length) { this.labelData = undefined; return; } @@ -485,7 +330,7 @@ export class TreemapSeries< } private applyGap(innerBox: _Scene.BBox, childBox: _Scene.BBox, allLeafNodes: boolean) { - const gap = allLeafNodes ? this.tile.gap * 0.5 : this.group.gap * 0.5; + const gap = allLeafNodes ? this.properties.tile.gap * 0.5 : this.properties.group.gap * 0.5; const getBounds = (box: _Scene.BBox): Record => ({ left: box.x, top: box.y, @@ -531,24 +376,18 @@ export class TreemapSeries< private getTileFormat(node: _ModuleSupport.HierarchyNode, isHighlighted: boolean): AgTreemapSeriesStyle { const { datum, depth, children } = node; - const { - tile, - group, - formatter, - ctx: { callbackCache }, - } = this; + const { colorKey, labelKey, secondaryLabelKey, sizeKey, tile, group, formatter } = this.properties; + if (!formatter || datum == null || depth == null) { return {}; } - const { colorKey, labelKey, secondaryLabelKey, sizeKey } = this; const isLeaf = children.length === 0; - const fill = (isLeaf ? tile.fill : group.fill) ?? node.fill; const stroke = (isLeaf ? tile.stroke : group.stroke) ?? node.stroke; const strokeWidth = isLeaf ? tile.strokeWidth : group.strokeWidth; - const result = callbackCache.call(formatter, { + const result = this.ctx.callbackCache.call(formatter, { seriesId: this.id, depth, datum, @@ -568,27 +407,26 @@ export class TreemapSeries< private getNodeFill(node: _ModuleSupport.HierarchyNode) { const isLeaf = node.children.length === 0; if (isLeaf) { - return this.tile.fill ?? node.fill; - } else { - const { undocumentedGroupFills } = this; - const defaultFill = undocumentedGroupFills[Math.min(node.depth ?? 0, undocumentedGroupFills.length)]; - return this.group.fill ?? defaultFill; + return this.properties.tile.fill ?? node.fill; } + const { undocumentedGroupFills } = this.properties; + const defaultFill = undocumentedGroupFills[Math.min(node.depth ?? 0, undocumentedGroupFills.length)]; + return this.properties.group.fill ?? defaultFill; } private getNodeStroke(node: _ModuleSupport.HierarchyNode) { const isLeaf = node.children.length === 0; if (isLeaf) { - return this.tile.stroke ?? node.stroke; - } else { - const { undocumentedGroupStrokes } = this; - const defaultStroke = undocumentedGroupStrokes[Math.min(node.depth ?? 0, undocumentedGroupStrokes.length)]; - return this.group.stroke ?? defaultStroke; + return this.properties.tile.stroke ?? node.stroke; } + const { undocumentedGroupStrokes } = this.properties; + const defaultStroke = undocumentedGroupStrokes[Math.min(node.depth ?? 0, undocumentedGroupStrokes.length)]; + return this.properties.group.stroke ?? defaultStroke; } async updateNodes() { - const { rootNode, data, highlightStyle, tile, group } = this; + const { rootNode, data } = this; + const { highlightStyle, tile, group } = this.properties; const { seriesRect } = this.chart ?? {}; if (!seriesRect || !data) return; @@ -599,7 +437,7 @@ export class TreemapSeries< let highlightedNode: _ModuleSupport.HierarchyNode | undefined = this.ctx.highlightManager?.getActiveHighlight() as any; - if (highlightedNode != null && !this.group.interactive && highlightedNode.children.length !== 0) { + if (highlightedNode != null && !this.properties.group.interactive && highlightedNode.children.length !== 0) { highlightedNode = undefined; } @@ -679,9 +517,9 @@ export class TreemapSeries< }; const formatting = formatLabels( labelDatum.label, - this.tile.label, + this.properties.tile.label, labelDatum.secondaryLabel, - this.tile.secondaryLabel, + this.properties.tile.secondaryLabel, { padding: tile.padding }, () => layout ); @@ -704,7 +542,7 @@ export class TreemapSeries< text: label.text, fontSize: label.fontSize, lineHeight: label.lineHeight, - style: this.tile.label, + style: this.properties.tile.label, x: labelX, y: labelYStart - (height - label.height) * 0.5, } @@ -715,7 +553,7 @@ export class TreemapSeries< text: secondaryLabel.text, fontSize: secondaryLabel.fontSize, lineHeight: secondaryLabel.fontSize, - style: this.tile.secondaryLabel, + style: this.properties.tile.secondaryLabel, x: labelX, y: labelYStart + (height - secondaryLabel.height) * 0.5, } @@ -738,7 +576,7 @@ export class TreemapSeries< text, fontSize: group.label.fontSize, lineHeight: AutoSizedLabel.lineHeight(group.label.fontSize), - style: this.group.label, + style: this.properties.group.label, x: bbox.x + padding + innerWidth * textAlignFactor, y: bbox.y + padding + groupTitleHeight * 0.5, }, @@ -814,6 +652,8 @@ export class TreemapSeries< } getTooltipHtml(node: _ModuleSupport.HierarchyNode): string { + const { datum, depth } = node; + const { id: seriesId } = this; const { tooltip, colorKey, @@ -822,11 +662,9 @@ export class TreemapSeries< secondaryLabelKey, sizeKey, sizeName = sizeKey, - id: seriesId, - } = this; - const { datum, depth } = node; + } = this.properties; const isLeaf = node.children.length === 0; - const interactive = isLeaf || this.group.interactive; + const interactive = isLeaf || this.properties.group.interactive; if (datum == null || depth == null || !interactive) { return ''; } @@ -836,7 +674,7 @@ export class TreemapSeries< const format = this.getTileFormat(node, false); const color = format?.fill ?? this.getNodeFill(node); - if (!tooltip.renderer && !tooltip.format && !title) { + if (!tooltip.renderer && !title) { return ''; } @@ -861,7 +699,7 @@ export class TreemapSeries< const defaults: AgTooltipRendererResult = { title, - color: isLeaf ? this.tile.label.color : this.group.label.color, + color: isLeaf ? this.properties.tile.label.color : this.properties.group.label.color, backgroundColor: color, content, }; diff --git a/packages/ag-charts-enterprise/src/series/treemap/treemapSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/treemap/treemapSeriesProperties.ts new file mode 100644 index 0000000000..0a302fe6f6 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/treemap/treemapSeriesProperties.ts @@ -0,0 +1,189 @@ +import type { + AgTreemapSeriesFormatterParams, + AgTreemapSeriesLabelFormatterParams, + AgTreemapSeriesOptions, + AgTreemapSeriesStyle, + AgTreemapSeriesTooltipRendererParams, + TextAlign, + VerticalAlign, +} from 'ag-charts-community'; +import { _ModuleSupport, _Scene } from 'ag-charts-community'; + +import { AutoSizeableSecondaryLabel, AutoSizedLabel } from '../util/labelFormatter'; + +const { Label } = _Scene; +const { + BaseProperties, + HierarchySeriesProperties, + HighlightStyle, + SeriesTooltip, + Validate, + BOOLEAN, + COLOR_STRING, + FUNCTION, + NUMBER, + OBJECT, + POSITIVE_NUMBER, + RATIO, + STRING, + STRING_ARRAY, + TEXT_ALIGN, + VERTICAL_ALIGN, +} = _ModuleSupport; + +class TreemapGroupLabel extends Label { + @Validate(NUMBER) + spacing: number = 0; +} + +class TreemapSeriesGroup extends BaseProperties { + @Validate(STRING, { optional: true }) + fill?: string; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(TEXT_ALIGN) + textAlign: TextAlign = 'center'; + + @Validate(POSITIVE_NUMBER) + gap: number = 0; + + @Validate(POSITIVE_NUMBER) + padding: number = 0; + + @Validate(BOOLEAN) + interactive: boolean = true; + + @Validate(OBJECT) + readonly label = new TreemapGroupLabel(); +} + +class TreemapSeriesTile extends BaseProperties { + @Validate(STRING, { optional: true }) + fill?: string; + + @Validate(RATIO) + fillOpacity: number = 1; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth: number = 1; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(TEXT_ALIGN) + textAlign: TextAlign = 'center'; + + @Validate(VERTICAL_ALIGN) + verticalAlign: VerticalAlign = 'middle'; + + @Validate(POSITIVE_NUMBER) + gap: number = 0; + + @Validate(POSITIVE_NUMBER) + padding: number = 0; + + @Validate(OBJECT) + readonly label = new AutoSizedLabel(); + + @Validate(OBJECT) + readonly secondaryLabel = new AutoSizeableSecondaryLabel(); +} + +class TreemapSeriesGroupHighlightStyle extends BaseProperties { + @Validate(STRING, { optional: true }) + fill?: string; + + @Validate(RATIO, { optional: true }) + fillOpacity?: number; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number; + + @Validate(RATIO, { optional: true }) + strokeOpacity?: number; + + @Validate(OBJECT) + readonly label = new AutoSizedLabel(); +} + +class TreemapSeriesTileHighlightStyle extends BaseProperties { + @Validate(STRING, { optional: true }) + fill?: string; + + @Validate(RATIO, { optional: true }) + fillOpacity?: number; + + @Validate(COLOR_STRING, { optional: true }) + stroke?: string; + + @Validate(POSITIVE_NUMBER, { optional: true }) + strokeWidth?: number; + + @Validate(RATIO, { optional: true }) + strokeOpacity?: number; + + @Validate(OBJECT) + readonly label = new AutoSizedLabel(); + + @Validate(OBJECT) + readonly secondaryLabel = new AutoSizeableSecondaryLabel(); +} + +class TreemapSeriesHighlightStyle extends HighlightStyle { + @Validate(OBJECT) + readonly group = new TreemapSeriesGroupHighlightStyle(); + + @Validate(OBJECT) + readonly tile = new TreemapSeriesTileHighlightStyle(); +} + +export class TreemapSeriesProperties extends HierarchySeriesProperties { + @Validate(STRING, { optional: true }) + sizeName?: string; + + @Validate(STRING, { optional: true }) + labelKey?: string; + + @Validate(STRING, { optional: true }) + secondaryLabelKey?: string; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgTreemapSeriesFormatterParams) => AgTreemapSeriesStyle; + + @Validate(OBJECT) + override readonly highlightStyle = new TreemapSeriesHighlightStyle(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip>(); + + @Validate(OBJECT) + readonly group = new TreemapSeriesGroup(); + + @Validate(OBJECT) + readonly tile = new TreemapSeriesTile(); + + // We haven't decided how to expose this yet, but we need to have this property, so it can change between light and dark themes + @Validate(STRING_ARRAY) + undocumentedGroupFills: string[] = []; + + // We haven't decided how to expose this yet, but we need to have this property, so it can change between light and dark themes + @Validate(STRING_ARRAY) + undocumentedGroupStrokes: string[] = []; +} diff --git a/packages/ag-charts-enterprise/src/series/util/labelFormatter.ts b/packages/ag-charts-enterprise/src/series/util/labelFormatter.ts index 907d577458..672314d59a 100644 --- a/packages/ag-charts-enterprise/src/series/util/labelFormatter.ts +++ b/packages/ag-charts-enterprise/src/series/util/labelFormatter.ts @@ -16,7 +16,7 @@ class BaseAutoSizedLabel extends Label { overflowStrategy: OverflowStrategy = 'ellipsis'; @Validate(NUMBER, { optional: true }) - minimumFontSize?: number = undefined; + minimumFontSize?: number; } export class AutoSizedLabel extends BaseAutoSizedLabel { diff --git a/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeries.ts b/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeries.ts index 483da1a499..ba3cf5d6f2 100644 --- a/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeries.ts +++ b/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeries.ts @@ -1,17 +1,11 @@ -import type { - AgTooltipRendererResult, - AgWaterfallSeriesFormat, - AgWaterfallSeriesFormatterParams, - AgWaterfallSeriesItemType, - AgWaterfallSeriesLabelFormatterParams, - AgWaterfallSeriesLabelPlacement, - AgWaterfallSeriesTooltipRendererParams, -} from 'ag-charts-community'; +import type { AgWaterfallSeriesItemType } from 'ag-charts-community'; import { _ModuleSupport, _Scene, _Util } from 'ag-charts-community'; +import type { WaterfallSeriesItem, WaterfallSeriesTotal } from './waterfallSeriesProperties'; +import { WaterfallSeriesProperties } from './waterfallSeriesProperties'; + const { adjustLabelPlacement, - Validate, SeriesNodePickMode, fixNumericExtent, valueProperty, @@ -19,14 +13,6 @@ const { accumulativeValueProperty, trailingAccumulatedValueProperty, ChartAxisDirection, - POSITIVE_NUMBER, - RATIO, - BOOLEAN, - STRING, - UNION, - FUNCTION, - COLOR_STRING, - LINE_DASH, getRectConfig, updateRect, checkCrisp, @@ -75,86 +61,6 @@ type WaterfallAnimationData = _ModuleSupport.CartesianAnimationData< WaterfallContext >; -class WaterfallSeriesItemTooltip { - @Validate(FUNCTION, { optional: true }) - renderer?: (params: AgWaterfallSeriesTooltipRendererParams) => string | AgTooltipRendererResult; -} - -class WaterfallSeriesLabel extends _Scene.Label { - @Validate(UNION(['start', 'end', 'inside'], 'a placement'), { optional: true }) - placement: AgWaterfallSeriesLabelPlacement = 'end'; - - @Validate(POSITIVE_NUMBER, { optional: true }) - padding: number = 6; -} - -class WaterfallSeriesItem { - readonly label = new WaterfallSeriesLabel(); - - tooltip: WaterfallSeriesItemTooltip = new WaterfallSeriesItemTooltip(); - - @Validate(FUNCTION, { optional: true }) - formatter?: (params: AgWaterfallSeriesFormatterParams) => AgWaterfallSeriesFormat; - - shadow?: _Scene.DropShadow = undefined; - - @Validate(STRING, { optional: true }) - name?: string = undefined; - - @Validate(COLOR_STRING, { optional: true }) - fill: string = '#c16068'; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = '#c16068'; - - @Validate(RATIO) - fillOpacity = 1; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 1; -} - -class WaterfallSeriesConnectorLine { - @Validate(BOOLEAN) - enabled = true; - - @Validate(COLOR_STRING, { optional: true }) - stroke: string = 'black'; - - @Validate(RATIO) - strokeOpacity = 1; - - @Validate(LINE_DASH, { optional: true }) - lineDash?: number[] = [0]; - - @Validate(POSITIVE_NUMBER) - lineDashOffset: number = 0; - - @Validate(POSITIVE_NUMBER) - strokeWidth: number = 2; -} - -class WaterfallSeriesItems { - readonly positive: WaterfallSeriesItem = new WaterfallSeriesItem(); - readonly negative: WaterfallSeriesItem = new WaterfallSeriesItem(); - readonly total: WaterfallSeriesItem = new WaterfallSeriesItem(); -} - -interface TotalMeta { - totalType: 'subtotal' | 'total'; - index: number; - axisLabel: any; -} - export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< _Scene.Rect, WaterfallNodeDatum, @@ -164,11 +70,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< static className = 'WaterfallSeries'; static type = 'waterfall' as const; - readonly item = new WaterfallSeriesItems(); - readonly line = new WaterfallSeriesConnectorLine(); - readonly totals: TotalMeta[] = []; - - tooltip = new _ModuleSupport.SeriesTooltip(); + override properties = new WaterfallSeriesProperties(); constructor(moduleCtx: _ModuleSupport.ModuleContext) { super({ @@ -193,30 +95,17 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< return direction; } - @Validate(STRING, { optional: true }) - xKey?: string = undefined; - - @Validate(STRING, { optional: true }) - xName?: string = undefined; - - @Validate(STRING, { optional: true }) - yKey?: string = undefined; - - @Validate(STRING, { optional: true }) - yName?: string = undefined; - - @Validate(POSITIVE_NUMBER) - cornerRadius: number = 0; - private seriesItemTypes: Set = new Set(['positive', 'negative', 'total']); protected smallestDataInterval?: { x: number; y: number } = undefined; override async processData(dataController: _ModuleSupport.DataController) { - const { xKey = '', yKey } = this; + const { xKey, yKey, totals } = this.properties; const { data = [] } = this; - if (!yKey) return; + if (!this.properties.isValid()) { + return; + } const positiveNumber = (v: any) => { return isContinuous(v) && v >= 0; @@ -237,7 +126,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< const dataWithTotals: any[] = []; - const totalsMap = this.totals.reduce((totalsMap, total) => { + const totalsMap = totals.reduce>((totalsMap, total) => { const totalsAtIndex = totalsMap.get(total.index); if (totalsAtIndex) { totalsAtIndex.push(total); @@ -245,27 +134,19 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< totalsMap.set(total.index, [total]); } return totalsMap; - }, new Map()); + }, new Map()); data.forEach((datum, i) => { dataWithTotals.push(datum); - const totalsAtIndex = totalsMap.get(i); - if (totalsAtIndex) { - // Use the `toString` method to make the axis labels unique as they're used as categories in the axis scale domain. - // Add random id property as there is caching for the axis label formatter result. If the label object is not unique, the axis label formatter will not be invoked. - totalsAtIndex.forEach((total) => - dataWithTotals.push({ - ...total, - [xKey]: total.axisLabel, - }) - ); - } + // Use the `toString` method to make the axis labels unique as they're used as categories in the axis scale domain. + // Add random id property as there is caching for the axis label formatter result. If the label object is not unique, the axis label formatter will not be invoked. + totalsMap.get(i)?.forEach((total) => dataWithTotals.push({ ...total.toJson(), [xKey]: total.axisLabel })); }); - const animationEnabled = !this.ctx.animationManager.isSkipped(); const isContinuousX = ContinuousScale.is(this.getCategoryAxis()?.scale); const extraProps = []; - if (animationEnabled) { + + if (!this.ctx.animationManager.isSkipped()) { extraProps.push(animationValidation(this)); } @@ -353,7 +234,8 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< } async createNodeData() { - const { data, dataModel, visible, line, smallestDataInterval } = this; + const { data, dataModel, smallestDataInterval } = this; + const { visible, line } = this.properties; const categoryAxis = this.getCategoryAxis(); const valueAxis = this.getValueAxis(); @@ -374,8 +256,10 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< const halfLineWidth = line.strokeWidth / 2; const offsetDirection = (barAlongX && !valueAxisReversed) || (!barAlongX && valueAxisReversed) ? -1 : 1; const offset = offsetDirection * halfLineWidth; - const { yKey = '', xKey = '', processedData } = this; - if (processedData?.type !== 'ungrouped') return []; + + if (this.processedData?.type !== 'ungrouped') { + return []; + } const contexts: WaterfallContext[] = []; @@ -425,7 +309,9 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< } let trailingSubtotal = 0; - processedData?.data.forEach(({ keys, datum, values }, dataIndex) => { + const { xKey, yKey, xName, yName } = this.properties; + + this.processedData?.data.forEach(({ keys, datum, values }, dataIndex) => { const datumType = values[totalTypeIndex]; const isSubtotal = this.isSubtotal(datumType); @@ -519,8 +405,8 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< datum, xKey, yKey, - xName: this.xName, - yName: this.yName, + xName, + yName, }, (value) => (isNumber(value) ? value.toFixed(2) : String(value)) ); @@ -559,7 +445,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< contexts[contextIndex].labelData.push(nodeDatum); }); - const connectorLinesEnabled = this.line.enabled; + const connectorLinesEnabled = this.properties.line.enabled; if (contexts.length > 0 && yCurrIndex !== undefined && connectorLinesEnabled) { contexts[0].pointData = pointData; } @@ -622,14 +508,14 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< private getItemConfig(seriesItemType: AgWaterfallSeriesItemType): WaterfallSeriesItem { switch (seriesItemType) { case 'positive': { - return this.item.positive; + return this.properties.item.positive; } case 'negative': { - return this.item.negative; + return this.properties.item.negative; } case 'subtotal': case 'total': { - return this.item.total; + return this.properties.item.total; } } } @@ -648,12 +534,11 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< isHighlight: boolean; }) { const { datumSelection, isHighlight } = opts; + const { id: seriesId, ctx } = this; const { - yKey = '', + yKey, highlightStyle: { item: itemHighlightStyle }, - id: seriesId, - ctx, - } = this; + } = this.properties; const categoryAxis = this.getCategoryAxis(); const crisp = checkCrisp(categoryAxis?.visibleRange); @@ -680,7 +565,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< lineDashOffset, fillShadow, strokeWidth: this.getStrokeWidth(strokeWidth), - cornerRadius: this.cornerRadius, + cornerRadius: this.properties.cornerRadius, cornerRadiusBbox: undefined, }; const visible = categoryAlongX ? datum.width > 0 : datum.height > 0; @@ -727,16 +612,15 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< } getTooltipHtml(nodeDatum: WaterfallNodeDatum): string { - const { xKey, yKey } = this; - const categoryAxis = this.getCategoryAxis(); const valueAxis = this.getValueAxis(); - if (!xKey || !yKey || !categoryAxis || !valueAxis) { + if (!this.properties.isValid() || !categoryAxis || !valueAxis) { return ''; } - const { xName, yName, id: seriesId } = this; + const { id: seriesId } = this; + const { xKey, yKey, xName, yName, tooltip } = this.properties; const { datum, itemId, xValue, yValue } = nodeDatum; const { fill, strokeWidth, name, formatter } = this.getItemConfig(itemId); @@ -770,7 +654,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< `${sanitizeHtml(xName ?? xKey)}: ${xString}
` + `${sanitizeHtml(ySubheading)}: ${yString}`; - return this.tooltip.toTooltipHtml( + return tooltip.toTooltipHtml( { title, content, backgroundColor: color }, { seriesId, itemId, datum, xKey, yKey, xName, yName, color } ); @@ -804,9 +688,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< protected override toggleSeriesItem(): void {} override animateEmptyUpdateReady({ datumSelections, labelSelections, contextData, paths }: WaterfallAnimationData) { - const fns = prepareBarAnimationFunctions( - collapsedStartingBarPosition(this.direction === 'vertical', this.axes) - ); + const fns = prepareBarAnimationFunctions(collapsedStartingBarPosition(this.isVertical(), this.axes)); motion.fromToMotion(this.id, 'datums', this.ctx.animationManager, datumSelections, fns); seriesLabelFadeInAnimation(this, 'labels', this.ctx.animationManager, labelSelections); @@ -817,7 +699,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< } const [lineNode] = paths[contextDataIndex]; - if (this.direction === 'vertical') { + if (this.isVertical()) { this.animateConnectorLinesVertical(lineNode, pointData); } else { this.animateConnectorLinesHorizontal(lineNode, pointData); @@ -957,7 +839,7 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< } protected updateLineNode(lineNode: _Scene.Path) { - const { stroke, strokeWidth, strokeOpacity, lineDash, lineDashOffset } = this.line; + const { stroke, strokeWidth, strokeOpacity, lineDash, lineDashOffset } = this.properties.line; lineNode.setProperties({ fill: undefined, stroke, @@ -971,7 +853,8 @@ export class WaterfallSeries extends _ModuleSupport.AbstractBarSeries< } protected isLabelEnabled() { - return this.item.positive.label.enabled || this.item.negative.label.enabled || this.item.total.label.enabled; + const { positive, negative, total } = this.properties.item; + return positive.label.enabled || negative.label.enabled || total.label.enabled; } protected override onDataChange() {} diff --git a/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeriesProperties.ts b/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeriesProperties.ts new file mode 100644 index 0000000000..05655c2236 --- /dev/null +++ b/packages/ag-charts-enterprise/src/series/waterfall/waterfallSeriesProperties.ts @@ -0,0 +1,152 @@ +import type { + AgTooltipRendererResult, + AgWaterfallSeriesFormat, + AgWaterfallSeriesFormatterParams, + AgWaterfallSeriesLabelFormatterParams, + AgWaterfallSeriesLabelPlacement, + AgWaterfallSeriesOptions, + AgWaterfallSeriesTooltipRendererParams, +} from 'ag-charts-community'; +import { _ModuleSupport, _Scene } from 'ag-charts-community'; + +const { DropShadow, Label } = _Scene; +const { + AbstractBarSeriesProperties, + BaseProperties, + PropertiesArray, + SeriesTooltip, + Validate, + BOOLEAN, + COLOR_STRING, + FUNCTION, + LINE_DASH, + NUMBER, + OBJECT, + OBJECT_ARRAY, + POSITIVE_NUMBER, + RATIO, + STRING, + UNION, +} = _ModuleSupport; + +export class WaterfallSeriesTotal extends BaseProperties { + @Validate(UNION(['subtotal', 'total'], 'a total type')) + totalType!: 'subtotal' | 'total'; + + @Validate(NUMBER) + index!: number; + + @Validate(STRING) + axisLabel!: string; +} + +class WaterfallSeriesItemTooltip extends BaseProperties { + @Validate(FUNCTION, { optional: true }) + renderer?: (params: AgWaterfallSeriesTooltipRendererParams) => string | AgTooltipRendererResult; +} + +class WaterfallSeriesLabel extends Label { + @Validate(UNION(['start', 'end', 'inside'], 'a placement')) + placement: AgWaterfallSeriesLabelPlacement = 'end'; + + @Validate(POSITIVE_NUMBER) + padding: number = 6; +} + +export class WaterfallSeriesItem extends BaseProperties { + @Validate(STRING, { optional: true }) + name?: string; + + @Validate(COLOR_STRING) + fill: string = '#c16068'; + + @Validate(COLOR_STRING) + stroke: string = '#c16068'; + + @Validate(RATIO) + fillOpacity = 1; + + @Validate(RATIO) + strokeOpacity = 1; + + @Validate(LINE_DASH) + lineDash?: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 1; + + @Validate(FUNCTION, { optional: true }) + formatter?: (params: AgWaterfallSeriesFormatterParams) => AgWaterfallSeriesFormat; + + @Validate(OBJECT) + readonly shadow = new DropShadow().set({ enabled: false }); + + @Validate(OBJECT) + readonly label = new WaterfallSeriesLabel(); + + @Validate(OBJECT) + readonly tooltip = new WaterfallSeriesItemTooltip(); +} + +class WaterfallSeriesConnectorLine extends BaseProperties { + @Validate(BOOLEAN) + enabled: boolean = true; + + @Validate(COLOR_STRING) + stroke: string = 'black'; + + @Validate(RATIO) + strokeOpacity: number = 1; + + @Validate(LINE_DASH) + lineDash?: number[] = [0]; + + @Validate(POSITIVE_NUMBER) + lineDashOffset: number = 0; + + @Validate(POSITIVE_NUMBER) + strokeWidth: number = 2; +} + +class WaterfallSeriesItems extends BaseProperties { + @Validate(OBJECT) + readonly positive = new WaterfallSeriesItem(); + + @Validate(OBJECT) + readonly negative = new WaterfallSeriesItem(); + + @Validate(OBJECT) + readonly total = new WaterfallSeriesItem(); +} + +export class WaterfallSeriesProperties extends AbstractBarSeriesProperties { + @Validate(STRING) + xKey!: string; + + @Validate(STRING) + yKey!: string; + + @Validate(STRING, { optional: true }) + xName?: string; + + @Validate(STRING, { optional: true }) + yName?: string; + + @Validate(POSITIVE_NUMBER) + cornerRadius: number = 0; + + @Validate(OBJECT) + readonly item = new WaterfallSeriesItems(); + + @Validate(OBJECT_ARRAY) + readonly totals: WaterfallSeriesTotal[] = new PropertiesArray(WaterfallSeriesTotal); + + @Validate(OBJECT) + readonly line = new WaterfallSeriesConnectorLine(); + + @Validate(OBJECT) + readonly tooltip = new SeriesTooltip(); +}