cats-charts 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -475,6 +475,7 @@ class Charts {
475
475
  legendSelected = {};
476
476
  defaultColors = ['#22c55e', '#3b82f6', '#ef4444', '#6b7280'];
477
477
  defaultLegendColors = ['#3b82f6', '#FFC107', '#22c55e', '#ef4444'];
478
+ heatmapPivotColors = ['#dbeafe', '#93c5fd', '#3b82f6', '#1d4ed8', '#1e3a8a'];
478
479
  // buildGraphicLegend(total: number | null, config: any, chartInstance: any, chartType?: string) {
479
480
  // const pillHeight = 28;
480
481
  // const spacing = 14;
@@ -1236,6 +1237,44 @@ class Charts {
1236
1237
  return date.toLocaleDateString('en-US', { day: '2-digit', month: 'short' });
1237
1238
  }
1238
1239
  }
1240
+ formatHeatmapPivotDate(value, aggregation = 'day') {
1241
+ const date = value instanceof Date ? value : new Date(value);
1242
+ if (Number.isNaN(date.getTime()))
1243
+ return String(value);
1244
+ switch (aggregation) {
1245
+ case 'year':
1246
+ return String(date.getFullYear());
1247
+ case 'quarter':
1248
+ return `Q${Math.floor(date.getMonth() / 3) + 1} ${date.getFullYear()}`;
1249
+ case 'month':
1250
+ return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
1251
+ case 'week': {
1252
+ const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
1253
+ const dayOfWeek = utcDate.getUTCDay() || 7;
1254
+ utcDate.setUTCDate(utcDate.getUTCDate() + 4 - dayOfWeek);
1255
+ const year = utcDate.getUTCFullYear();
1256
+ const yearStart = new Date(Date.UTC(year, 0, 1));
1257
+ const week = Math.ceil(((utcDate.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);
1258
+ return `W${week} ${year}`;
1259
+ }
1260
+ case 'hour':
1261
+ return date.toLocaleDateString('en-US', {
1262
+ day: '2-digit',
1263
+ month: 'short',
1264
+ hour: '2-digit',
1265
+ hour12: false,
1266
+ });
1267
+ case 'minute':
1268
+ return date.toLocaleTimeString('en-US', {
1269
+ hour: '2-digit',
1270
+ minute: '2-digit',
1271
+ hour12: false,
1272
+ });
1273
+ case 'day':
1274
+ default:
1275
+ return date.toLocaleDateString('en-US', { day: '2-digit', month: 'short' });
1276
+ }
1277
+ }
1239
1278
  getIsoWeek(date) {
1240
1279
  const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
1241
1280
  const dayOfWeek = utcDate.getUTCDay() || 7;
@@ -4937,6 +4976,719 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
4937
4976
  type: Output
4938
4977
  }] } });
4939
4978
 
4979
+ echarts.use([
4980
+ HeatmapChart,
4981
+ GridComponent,
4982
+ LegendComponent,
4983
+ TitleComponent,
4984
+ TooltipComponent,
4985
+ VisualMapComponent,
4986
+ CanvasRenderer,
4987
+ ]);
4988
+ class HeatmapPivot {
4989
+ chartService;
4990
+ cdr;
4991
+ chartContainer;
4992
+ config = { xAxisData: [], yAxisData: [], series: [] };
4993
+ theme;
4994
+ events = [];
4995
+ contextMenuOptions = [];
4996
+ showContextMenu = false;
4997
+ contextMenuClick = new EventEmitter();
4998
+ chartEvent = new EventEmitter();
4999
+ legendEvent = new EventEmitter();
5000
+ chartOption = {};
5001
+ chartInstance;
5002
+ cardPosition = { x: 0, y: 0 };
5003
+ openCard = false;
5004
+ columnTotals = [];
5005
+ xAxisState;
5006
+ yAxisState;
5007
+ yAxisLabelWidth;
5008
+ totalLabelWidth;
5009
+ normalizedSeriesData = [];
5010
+ eventCleanup = [];
5011
+ constructor(chartService, cdr) {
5012
+ this.chartService = chartService;
5013
+ this.cdr = cdr;
5014
+ }
5015
+ get deafultColors() {
5016
+ return this.chartService.heatmapPivotColors;
5017
+ }
5018
+ get hasSideLegendWidth() {
5019
+ return this.isSideLegend && !!this.config.customLegend?.maxWidth;
5020
+ }
5021
+ get legendShellMaxWidth() {
5022
+ return this.isSideLegend ? (this.config.customLegend?.maxWidth ?? null) : null;
5023
+ }
5024
+ get isSideLegend() {
5025
+ return (this.config.customLegend?.position === 'left' ||
5026
+ this.config.customLegend?.position === 'right');
5027
+ }
5028
+ ngOnInit() {
5029
+ this.buildOption();
5030
+ }
5031
+ ngOnChanges(changes) {
5032
+ if (!this.config)
5033
+ return;
5034
+ this.buildOption();
5035
+ if (changes['events'] || changes['showContextMenu']) {
5036
+ this.bindEvents();
5037
+ }
5038
+ // NgxEchartsDirective recreates the chart when its theme input changes.
5039
+ }
5040
+ buildOption() {
5041
+ const config = this.config;
5042
+ this.xAxisState = this.buildAxisState('x');
5043
+ this.yAxisState = this.buildAxisState('y');
5044
+ const yAxisLabelWidth = this.resolveYAxisLabelWidth();
5045
+ this.yAxisLabelWidth = yAxisLabelWidth;
5046
+ this.totalLabelWidth = this.resolveTotalLabelWidth(yAxisLabelWidth);
5047
+ this.columnTotals = this.xAxisState.values.map(() => 0);
5048
+ const series = (config.series ?? []).map((item) => ({
5049
+ ...item,
5050
+ type: 'heatmap',
5051
+ coordinateSystem: 'cartesian2d',
5052
+ encode: { x: 0, y: 1, tooltip: [2] },
5053
+ data: this.normalizeData(item.data ?? []),
5054
+ itemStyle: {
5055
+ borderColor: '#ffffff',
5056
+ borderWidth: Math.max(config.cellGap ?? 6, 0),
5057
+ borderRadius: Math.max(config.cellBorderRadius ?? 3, 0),
5058
+ ...item['itemStyle'],
5059
+ },
5060
+ label: {
5061
+ show: config.showLabel ?? true,
5062
+ position: 'inside',
5063
+ color: config.labelColor ?? '#252a31',
5064
+ fontSize: 14,
5065
+ formatter: config.labelFormatter ??
5066
+ ((params) => {
5067
+ const value = params.value?.[2];
5068
+ return typeof value === 'number' ? this.chartService.compactValueFormat(value) : '-';
5069
+ }),
5070
+ ...item['label'],
5071
+ },
5072
+ emphasis: { disabled: true, ...item['emphasis'] },
5073
+ }));
5074
+ this.normalizedSeriesData = series.map((item) => item.data);
5075
+ let min = Infinity;
5076
+ let max = -Infinity;
5077
+ for (const item of series) {
5078
+ for (const dataItem of item.data) {
5079
+ const point = this.pointValue(dataItem);
5080
+ const value = point[2];
5081
+ if (!Number.isFinite(value))
5082
+ continue;
5083
+ min = Math.min(min, value);
5084
+ max = Math.max(max, value);
5085
+ const column = Number(point[0]);
5086
+ if (this.columnTotals[column] !== undefined)
5087
+ this.columnTotals[column] += value;
5088
+ }
5089
+ }
5090
+ this.chartOption = {
5091
+ backgroundColor: config.backgroundColor ?? 'transparent',
5092
+ color: config.colors ?? this.deafultColors,
5093
+ title: [
5094
+ {
5095
+ show: config.showTitle ?? !!config.title,
5096
+ text: config.title,
5097
+ left: config.titlePosition ?? 'center',
5098
+ },
5099
+ ...(config.showTotals !== false
5100
+ ? [
5101
+ {
5102
+ id: 'heatmap-pivot-total-label',
5103
+ text: config.totalLabel ?? 'Total',
5104
+ left: 8,
5105
+ bottom: 12,
5106
+ padding: 0,
5107
+ textStyle: {
5108
+ color: '#182230',
5109
+ fontFamily: 'Arial, sans-serif',
5110
+ fontSize: 14,
5111
+ fontWeight: 600,
5112
+ width: this.totalLabelWidth,
5113
+ overflow: 'truncate',
5114
+ ellipsis: '...',
5115
+ },
5116
+ },
5117
+ ]
5118
+ : []),
5119
+ ],
5120
+ tooltip: {
5121
+ show: config.showTooltip ?? true,
5122
+ trigger: 'item',
5123
+ backgroundColor: '#ffffff',
5124
+ borderWidth: 0,
5125
+ padding: [10, 12],
5126
+ confine: true,
5127
+ textStyle: { color: '#434a51', fontFamily: 'Arial, sans-serif', fontSize: 12 },
5128
+ borderRadius: 6,
5129
+ shadowBlur: 16,
5130
+ shadowColor: 'rgba(16,24,40,0.12)',
5131
+ shadowOffsetY: 5,
5132
+ formatter: (params) => config.tooltipFormatter
5133
+ ? config.tooltipFormatter(params, config)
5134
+ : this.formatTooltip(params),
5135
+ ...config.tooltip,
5136
+ },
5137
+ legend: {
5138
+ show: !config.hideLegend && !config.customLegend,
5139
+ type: 'scroll',
5140
+ top: (config.showTitle ?? !!config.title) ? 30 : 0,
5141
+ icon: 'roundRect',
5142
+ textStyle: { color: '#434a51', fontSize: 14 },
5143
+ ...config.legend,
5144
+ data: config.legend?.data ?? series.map((item) => item.name),
5145
+ selected: { ...this.readLegendSelected(), ...config.legend?.selected },
5146
+ selectedMode: config.legend?.selectedMode ??
5147
+ (config.customLegend?.selectionMode === 'exclusive' ? 'single' : true),
5148
+ },
5149
+ grid: [
5150
+ {
5151
+ left: this.labelGridLeft(yAxisLabelWidth),
5152
+ right: 0,
5153
+ top: 0,
5154
+ bottom: config.showTotals !== false ? 41 : 0,
5155
+ ...config.grid,
5156
+ },
5157
+ ...(config.showTotals !== false
5158
+ ? [
5159
+ {
5160
+ left: 0,
5161
+ right: 0,
5162
+ bottom: 1,
5163
+ height: 40,
5164
+ containLabel: false,
5165
+ },
5166
+ ]
5167
+ : []),
5168
+ ],
5169
+ xAxis: [
5170
+ {
5171
+ position: 'top',
5172
+ axisLine: { show: false },
5173
+ axisTick: { show: false },
5174
+ splitLine: { show: false },
5175
+ ...this.axisOptions('x'),
5176
+ axisLabel: {
5177
+ interval: 'auto',
5178
+ hideOverlap: true,
5179
+ margin: 14,
5180
+ color: '#434a51',
5181
+ fontFamily: 'Arial, sans-serif',
5182
+ fontSize: 14,
5183
+ formatter: this.xAxisState.type === 'time'
5184
+ ? (value) => this.formatAxisValue('x', value)
5185
+ : undefined,
5186
+ ...config.xAxis?.axisLabel,
5187
+ },
5188
+ },
5189
+ ...this.totalsAxisOptions(),
5190
+ ],
5191
+ yAxis: [
5192
+ {
5193
+ inverse: true,
5194
+ axisLine: { show: false },
5195
+ axisTick: { show: false },
5196
+ splitLine: { show: false },
5197
+ ...this.axisOptions('y'),
5198
+ axisLabel: {
5199
+ interval: 0,
5200
+ margin: yAxisLabelWidth + 16,
5201
+ align: 'left',
5202
+ color: '#434a51',
5203
+ fontFamily: 'Arial, sans-serif',
5204
+ fontSize: 14,
5205
+ formatter: this.yAxisState.type === 'time'
5206
+ ? (value) => this.formatAxisValue('y', value)
5207
+ : undefined,
5208
+ ...config.yAxis?.axisLabel,
5209
+ width: yAxisLabelWidth,
5210
+ overflow: config.yAxis?.axisLabel?.overflow ?? 'truncate',
5211
+ ellipsis: config.yAxis?.axisLabel?.ellipsis ?? '...',
5212
+ },
5213
+ },
5214
+ ...(config.showTotals !== false
5215
+ ? [
5216
+ {
5217
+ gridIndex: 1,
5218
+ type: 'value',
5219
+ min: 0,
5220
+ max: 1,
5221
+ show: false,
5222
+ },
5223
+ ]
5224
+ : []),
5225
+ ],
5226
+ visualMap: this.buildVisualMaps(min, max),
5227
+ series,
5228
+ };
5229
+ }
5230
+ resolveYAxisLabelWidth() {
5231
+ const style = this.config.yAxis?.axisLabel;
5232
+ const chartWidth = this.chartContainer?.nativeElement.getBoundingClientRect().width
5233
+ || (this.chartInstance && !this.chartInstance.isDisposed() ? this.chartInstance.getWidth() : 0)
5234
+ || 360;
5235
+ const defaultMaxWidth = Math.max(100, Math.min(240, Math.round(chartWidth * 0.22)));
5236
+ const minWidth = Math.max(0, this.config.yAxisLabelMinWidth
5237
+ ?? Math.min(50, this.config.yAxisLabelMaxWidth ?? defaultMaxWidth));
5238
+ const maxWidth = Math.max(minWidth, this.config.yAxisLabelMaxWidth ?? defaultMaxWidth);
5239
+ if (typeof style?.width === 'number' && Number.isFinite(style.width)) {
5240
+ return Math.min(maxWidth, Math.max(minWidth, style.width));
5241
+ }
5242
+ const font = `${style?.fontWeight ?? 400} ${style?.fontSize ?? 14}px ${style?.fontFamily ?? 'Arial, sans-serif'}`;
5243
+ let width = minWidth;
5244
+ this.yAxisState.values.forEach((_value, index) => {
5245
+ width = Math.max(width, echarts.format.getTextRect(this.formatAxisValue('y', index), font).width);
5246
+ });
5247
+ if (this.config.showTotals !== false) {
5248
+ width = Math.max(width, echarts.format.getTextRect(this.config.totalLabel ?? 'Total', '600 14px Arial, sans-serif').width);
5249
+ }
5250
+ return Math.min(maxWidth, width);
5251
+ }
5252
+ labelGridLeft(width) {
5253
+ const yAxisHidden = this.config.yAxis?.['show'] === false || this.config.yAxis?.axisLabel?.show === false;
5254
+ return yAxisHidden && this.config.showTotals === false ? 0 : width + 16;
5255
+ }
5256
+ resolveTotalLabelWidth(labelWidth) {
5257
+ const configuredLeft = this.config.grid?.left;
5258
+ const chartWidth = this.chartContainer?.nativeElement.getBoundingClientRect().width
5259
+ || (this.chartInstance && !this.chartInstance.isDisposed() ? this.chartInstance.getWidth() : 0)
5260
+ || 360;
5261
+ const defaultLeft = this.labelGridLeft(labelWidth);
5262
+ const left = configuredLeft === undefined ? defaultLeft
5263
+ : typeof configuredLeft === 'number' ? configuredLeft
5264
+ : configuredLeft.endsWith('%') ? parseFloat(configuredLeft) / 100 * chartWidth
5265
+ : parseFloat(configuredLeft);
5266
+ // Reserve eight pixels on each side of the heading before the total values.
5267
+ return Math.max(0, Math.min(labelWidth, (Number.isFinite(left) ? left : defaultLeft) - 16));
5268
+ }
5269
+ buildVisualMaps(min, max) {
5270
+ const config = this.config;
5271
+ const base = {
5272
+ show: false,
5273
+ min: config.min ?? (Number.isFinite(min) ? min : 0),
5274
+ max: config.max ?? (Number.isFinite(max) ? max : 100),
5275
+ dimension: 2,
5276
+ calculable: true,
5277
+ orient: 'horizontal',
5278
+ left: 'center',
5279
+ bottom: 0,
5280
+ inRange: { color: config.colors ?? this.deafultColors },
5281
+ };
5282
+ const rules = this.visualMapOptions();
5283
+ if (rules['type'] !== 'piecewise' && !rules['pieces'] && !rules['categories']) {
5284
+ return { ...base, ...rules };
5285
+ }
5286
+ return [
5287
+ { ...base, type: 'continuous', show: false },
5288
+ {
5289
+ ...base,
5290
+ ...rules,
5291
+ type: 'piecewise',
5292
+ inRange: {
5293
+ colorAlpha: 1,
5294
+ ...base.inRange,
5295
+ ...(rules['inRange'] && typeof rules['inRange'] === 'object' ? rules['inRange'] : {}),
5296
+ },
5297
+ // An empty mapping preserves the color assigned by the continuous map.
5298
+ outOfRange: rules['outOfRange'] ?? {},
5299
+ },
5300
+ ];
5301
+ }
5302
+ visualMapOptions() {
5303
+ const config = this.config.visualMap;
5304
+ if (!config?.pieces)
5305
+ return config ?? {};
5306
+ return {
5307
+ ...config,
5308
+ type: config.type ?? 'piecewise',
5309
+ pieces: config.pieces.flatMap((piece) => {
5310
+ const { eq, neq, ne, opacity, ...options } = piece;
5311
+ const native = {
5312
+ ...options,
5313
+ ...(opacity !== undefined ? {
5314
+ colorAlpha: Math.min(100, Math.max(0, Number.isFinite(opacity) ? opacity : 100)) / 100,
5315
+ } : {}),
5316
+ };
5317
+ if (eq !== undefined)
5318
+ return [{ ...native, value: eq }];
5319
+ const excluded = neq ?? ne;
5320
+ if (excluded === undefined)
5321
+ return [native];
5322
+ if (native.value !== undefined)
5323
+ return native.value === excluded ? [] : [native];
5324
+ const { gt, gte, min, lt, lte, max, ...style } = native;
5325
+ const lower = Math.max(gt ?? -Infinity, gte ?? -Infinity, min ?? -Infinity);
5326
+ const upper = Math.min(lt ?? Infinity, lte ?? Infinity, max ?? Infinity);
5327
+ const lowerOpen = gt === lower;
5328
+ const upperOpen = lt === upper;
5329
+ const range = (start, end, startOpen, endOpen) => {
5330
+ if (start > end || (start === end && (startOpen || endOpen)))
5331
+ return [];
5332
+ return [
5333
+ {
5334
+ ...style,
5335
+ ...(Number.isFinite(start)
5336
+ ? startOpen && start === excluded
5337
+ ? // ECharts joins touching pieces; use the next representable number
5338
+ // so the excluded value cannot be reintroduced at their boundary.
5339
+ { gte: this.nextNumericValue(start) }
5340
+ : { [startOpen ? 'gt' : 'gte']: start }
5341
+ : {}),
5342
+ ...(Number.isFinite(end) ? { [endOpen ? 'lt' : 'lte']: end } : {}),
5343
+ },
5344
+ ];
5345
+ };
5346
+ return [
5347
+ ...range(lower, Math.min(upper, excluded), lowerOpen, excluded <= upper || upperOpen),
5348
+ ...range(Math.max(lower, excluded), upper, excluded >= lower || lowerOpen, upperOpen),
5349
+ ];
5350
+ }),
5351
+ };
5352
+ }
5353
+ nextNumericValue(value) {
5354
+ if (value === 0)
5355
+ return Number.MIN_VALUE;
5356
+ const buffer = new DataView(new ArrayBuffer(8));
5357
+ buffer.setFloat64(0, value);
5358
+ buffer.setBigUint64(0, buffer.getBigUint64(0) + (value > 0 ? 1n : -1n));
5359
+ return buffer.getFloat64(0);
5360
+ }
5361
+ totalsAxisOptions() {
5362
+ if (this.config.showTotals === false)
5363
+ return [];
5364
+ const options = this.axisOptions('x');
5365
+ const axisLine = { show: true, onZero: false, lineStyle: { color: '#e4e7ec', width: 1 } };
5366
+ const borderAxis = {
5367
+ gridIndex: 1,
5368
+ z: 10,
5369
+ type: 'category',
5370
+ data: [''],
5371
+ boundaryGap: true,
5372
+ axisLine,
5373
+ axisLabel: { show: false },
5374
+ axisTick: { show: false },
5375
+ splitLine: { show: false },
5376
+ axisPointer: { show: false },
5377
+ };
5378
+ const base = {
5379
+ ...options,
5380
+ show: true,
5381
+ position: 'bottom',
5382
+ axisLine: { show: false },
5383
+ axisTick: { show: false },
5384
+ splitLine: { show: false },
5385
+ axisPointer: { show: false },
5386
+ };
5387
+ return [
5388
+ {
5389
+ ...base,
5390
+ axisLabel: {
5391
+ show: true,
5392
+ interval: 0,
5393
+ margin: 13,
5394
+ color: '#182230',
5395
+ fontFamily: 'Arial, sans-serif',
5396
+ fontSize: 14,
5397
+ fontWeight: 600,
5398
+ formatter: (value) => {
5399
+ const index = this.xAxisState.values.findIndex((bucket) => String(bucket) === value);
5400
+ const total = this.columnTotals[index] ?? 0;
5401
+ return this.config.totalFormatter ? this.config.totalFormatter(total) : String(total);
5402
+ },
5403
+ },
5404
+ },
5405
+ { ...borderAxis, position: 'top' },
5406
+ { ...borderAxis, position: 'bottom' },
5407
+ ];
5408
+ }
5409
+ pointValue(point) {
5410
+ return Array.isArray(point) ? point : point.value;
5411
+ }
5412
+ normalizeData(data) {
5413
+ return data.flatMap((point) => {
5414
+ const [x, y, value] = this.pointValue(point);
5415
+ const normalized = [
5416
+ this.normalizeCoordinate('x', x),
5417
+ this.normalizeCoordinate('y', y),
5418
+ value,
5419
+ ];
5420
+ if (!normalized.every(Number.isFinite) || normalized[0] < 0 || normalized[1] < 0)
5421
+ return [];
5422
+ return [Array.isArray(point) ? normalized : { ...point, value: normalized }];
5423
+ });
5424
+ }
5425
+ buildAxisState(axis) {
5426
+ const options = axis === 'x' ? this.config.xAxis : this.config.yAxis;
5427
+ const axisData = axis === 'x' ? this.config.xAxisData : this.config.yAxisData;
5428
+ const type = options?.type ?? 'category';
5429
+ const coordinates = (this.config.series ?? []).flatMap((series) => (series.data ?? []).map((point) => this.pointValue(point)[axis === 'x' ? 0 : 1]));
5430
+ const resolvedCoordinates = coordinates.map((value) => typeof value === 'number' &&
5431
+ Number.isInteger(value) &&
5432
+ value >= 0 &&
5433
+ value < (axisData?.length ?? 0)
5434
+ ? axisData[value]
5435
+ : value);
5436
+ const axisValues = type === 'time'
5437
+ ? [...(axisData ?? []), ...resolvedCoordinates]
5438
+ : axisData?.length
5439
+ ? axisData
5440
+ : coordinates;
5441
+ const values = [
5442
+ ...new Set(axisValues.map((value) => (type === 'time' ? this.timestamp(value) : String(value)))),
5443
+ ].filter((value) => type !== 'time' || Number.isFinite(value));
5444
+ if (type === 'time')
5445
+ values.sort((a, b) => a - b);
5446
+ return { type, values };
5447
+ }
5448
+ timestamp(value) {
5449
+ return value instanceof Date ? value.getTime() : new Date(value).getTime();
5450
+ }
5451
+ normalizeCoordinate(axis, value) {
5452
+ const state = axis === 'x' ? this.xAxisState : this.yAxisState;
5453
+ const axisData = axis === 'x' ? this.config.xAxisData : this.config.yAxisData;
5454
+ const indexed = typeof value === 'number' &&
5455
+ Number.isInteger(value) &&
5456
+ value >= 0 &&
5457
+ value < (axisData?.length ?? 0);
5458
+ const resolved = indexed ? axisData[value] : value;
5459
+ return state.values.indexOf(state.type === 'time' ? this.timestamp(resolved) : String(resolved));
5460
+ }
5461
+ axisOptions(axis) {
5462
+ const state = axis === 'x' ? this.xAxisState : this.yAxisState;
5463
+ const options = (axis === 'x' ? this.config.xAxis : this.config.yAxis) ?? {};
5464
+ const { type, minInterval, min, max, ...styles } = options;
5465
+ let bucketMin = min;
5466
+ let bucketMax = max;
5467
+ if (state.type === 'time') {
5468
+ if (min !== undefined) {
5469
+ const timestamp = this.timestamp(min);
5470
+ bucketMin =
5471
+ min === 'dataMin' ? 0 : state.values.findIndex((value) => Number(value) >= timestamp);
5472
+ if (bucketMin === -1)
5473
+ bucketMin = state.values.length;
5474
+ }
5475
+ if (max !== undefined) {
5476
+ const timestamp = this.timestamp(max);
5477
+ bucketMax =
5478
+ max === 'dataMax'
5479
+ ? state.values.length - 1
5480
+ : state.values.reduce((last, value, index) => (Number(value) <= timestamp ? index : last), -1);
5481
+ }
5482
+ }
5483
+ // Native Cartesian heatmaps require category axes. Time values remain sorted
5484
+ // buckets and use the pivot date formatter for their displayed labels.
5485
+ return {
5486
+ ...styles,
5487
+ type: 'category',
5488
+ boundaryGap: true,
5489
+ data: state.values.map(String),
5490
+ min: bucketMin,
5491
+ max: bucketMax,
5492
+ };
5493
+ }
5494
+ formatAxisValue(axis, value) {
5495
+ const state = axis === 'x' ? this.xAxisState : this.yAxisState;
5496
+ const index = typeof value === 'number'
5497
+ ? value
5498
+ : state.values.findIndex((bucket) => String(bucket) === value);
5499
+ const resolved = state.values[index] ?? value;
5500
+ if (state.type === 'time') {
5501
+ return this.chartService.formatHeatmapPivotDate(resolved, axis === 'x' ? this.config.XAxisAggregation : this.config.YAxisAggregation);
5502
+ }
5503
+ return String(resolved);
5504
+ }
5505
+ formatTooltip(params) {
5506
+ const [x, y] = params.value ?? [];
5507
+ const rows = [
5508
+ ['Row Dimension', this.formatAxisValue('x', x)],
5509
+ ['Column Dimension', this.formatAxisValue('y', y)],
5510
+ ];
5511
+ const selected = this.readLegendSelected();
5512
+ this.normalizedSeriesData.forEach((data, index) => {
5513
+ const series = this.config.series[index];
5514
+ if (selected[series.name] === false)
5515
+ return;
5516
+ const match = data.find((point) => {
5517
+ const value = this.pointValue(point);
5518
+ return value[0] === x && value[1] === y;
5519
+ });
5520
+ if (match) {
5521
+ rows.push([
5522
+ series.name,
5523
+ String(this.chartService.formatTooltipValue(this.pointValue(match)[2], this.config.tooltipCompactValue)),
5524
+ ]);
5525
+ }
5526
+ });
5527
+ const extra = params.data?.[this.config.extraTooltipKey ?? 'tooltip'];
5528
+ if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
5529
+ for (const [key, value] of Object.entries(extra)) {
5530
+ rows.push([
5531
+ key,
5532
+ typeof value === 'number'
5533
+ ? String(this.chartService.formatTooltipValue(value, this.config.tooltipCompactValue))
5534
+ : String(value ?? '-'),
5535
+ ]);
5536
+ }
5537
+ }
5538
+ return `<div style="display:grid;grid-template-columns:112px 8px minmax(170px,1fr);row-gap:5px;line-height:16px">${rows
5539
+ .map(([label, content]) => `<span style="font-weight:600">${echarts.format.encodeHTML(String(label))}</span><span>:</span><span>${echarts.format.encodeHTML(String(content ?? '-'))}</span>`)
5540
+ .join('')}</div>`;
5541
+ }
5542
+ onChartInit(chart) {
5543
+ this.unbindEvents();
5544
+ this.chartInstance = chart;
5545
+ // Apply the width after ngx-echarts has installed the initial chart options.
5546
+ queueMicrotask(() => {
5547
+ if (this.chartInstance === chart)
5548
+ this.refreshLabelWidth();
5549
+ });
5550
+ this.bindEvents();
5551
+ this.cdr.markForCheck();
5552
+ }
5553
+ refreshLabelWidth() {
5554
+ const chart = this.chartInstance;
5555
+ if (!chart || chart.isDisposed() || !this.yAxisState)
5556
+ return;
5557
+ const width = this.resolveYAxisLabelWidth();
5558
+ const totalWidth = this.resolveTotalLabelWidth(width);
5559
+ if (width === this.yAxisLabelWidth && totalWidth === this.totalLabelWidth)
5560
+ return;
5561
+ const update = {
5562
+ yAxis: [{ axisLabel: {
5563
+ width,
5564
+ margin: this.config.yAxis?.axisLabel?.margin ?? width + 16,
5565
+ } }],
5566
+ };
5567
+ if (this.config.showTotals !== false) {
5568
+ update['title'] = [{ id: 'heatmap-pivot-total-label', textStyle: { width: totalWidth } }];
5569
+ }
5570
+ if (this.config.grid?.left === undefined) {
5571
+ update['grid'] = [{
5572
+ left: this.labelGridLeft(width),
5573
+ }];
5574
+ }
5575
+ chart.setOption(update, { lazyUpdate: true, silent: true });
5576
+ this.yAxisLabelWidth = width;
5577
+ this.totalLabelWidth = totalWidth;
5578
+ }
5579
+ bindEvents() {
5580
+ this.unbindEvents();
5581
+ const chart = this.chartInstance;
5582
+ if (!chart || chart.isDisposed())
5583
+ return;
5584
+ const eventNames = new Set(this.events);
5585
+ if (this.showContextMenu)
5586
+ eventNames.add('contextmenu');
5587
+ for (const eventName of eventNames) {
5588
+ const handler = (params) => {
5589
+ if (eventName === 'contextmenu') {
5590
+ this.openContextMenu(params?.event?.event);
5591
+ }
5592
+ this.chartEvent.emit({ type: eventName, data: params });
5593
+ };
5594
+ chart.on(eventName, handler);
5595
+ this.eventCleanup.push(() => {
5596
+ if (!chart.isDisposed())
5597
+ chart.off(eventName, handler);
5598
+ });
5599
+ if (eventName === 'click' || eventName === 'contextmenu') {
5600
+ const zr = chart.getZr();
5601
+ const backgroundHandler = (event) => {
5602
+ if (event.target)
5603
+ return;
5604
+ if (eventName === 'contextmenu') {
5605
+ this.openContextMenu(event.event);
5606
+ this.chartEvent.emit({
5607
+ type: 'contextmenu',
5608
+ data: { ...event, isZrEvent: true },
5609
+ });
5610
+ }
5611
+ else {
5612
+ this.chartEvent.emit({ type: 'blur', data: { ...event, type: 'blur' } });
5613
+ }
5614
+ };
5615
+ zr.on(eventName, backgroundHandler);
5616
+ this.eventCleanup.push(() => {
5617
+ if (!chart.isDisposed())
5618
+ zr.off(eventName, backgroundHandler);
5619
+ });
5620
+ }
5621
+ }
5622
+ }
5623
+ openContextMenu(event) {
5624
+ if (!event || !this.chartInstance || this.chartInstance.isDisposed())
5625
+ return;
5626
+ event.preventDefault();
5627
+ const rect = this.chartInstance.getDom().getBoundingClientRect();
5628
+ this.cardPosition = { x: event.clientX - rect.left, y: event.clientY - rect.top };
5629
+ this.openCard = this.showContextMenu || this.contextMenuOptions.length > 0;
5630
+ this.cdr.markForCheck();
5631
+ }
5632
+ onMenuClick(item) {
5633
+ this.contextMenuClick.emit(item);
5634
+ this.closeCard();
5635
+ }
5636
+ closeCard() {
5637
+ this.openCard = false;
5638
+ }
5639
+ onLegendEvent(event) {
5640
+ this.legendEvent.emit(event);
5641
+ this.chartEvent.emit({ type: `legend:${event.type}`, data: event });
5642
+ }
5643
+ readLegendSelected() {
5644
+ if (!this.chartInstance || this.chartInstance.isDisposed())
5645
+ return {};
5646
+ const option = this.chartInstance.getOption();
5647
+ return { ...option.legend?.[0]?.selected };
5648
+ }
5649
+ unbindEvents() {
5650
+ this.eventCleanup.forEach((cleanup) => cleanup());
5651
+ this.eventCleanup = [];
5652
+ }
5653
+ ngOnDestroy() {
5654
+ this.unbindEvents();
5655
+ if (this.chartInstance && !this.chartInstance.isDisposed()) {
5656
+ this.chartInstance.dispose();
5657
+ }
5658
+ }
5659
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: HeatmapPivot, deps: [{ token: Charts }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
5660
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: HeatmapPivot, isStandalone: true, selector: "lib-heatmap-pivot", inputs: { config: "config", theme: "theme", events: "events", contextMenuOptions: "contextMenuOptions", showContextMenu: "showContextMenu" }, outputs: { contextMenuClick: "contextMenuClick", chartEvent: "chartEvent", legendEvent: "legendEvent" }, host: { listeners: { "window:resize": "refreshLabelWidth()", "document:click": "closeCard()" } }, providers: [provideEchartsCore({ echarts })], viewQueries: [{ propertyName: "chartContainer", first: true, predicate: ["chartContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"heatmap-pivot-chart-shell\"\n [class.heatmap-pivot-chart-shell--legend-left]=\"config.customLegend?.position === 'left'\"\n [class.heatmap-pivot-chart-shell--legend-right]=\"config.customLegend?.position === 'right'\"\n [class.heatmap-pivot-chart-shell--legend-bottom]=\"config.customLegend?.position === 'bottom'\"\n>\n @if (\n config.customLegend && !config.hideLegend &&\n config.customLegend.position !== 'bottom' &&\n config.customLegend.position !== 'right'\n ) {\n <lib-chart-legend\n class=\"heatmap-pivot-chart-shell__legend\"\n [class.heatmap-pivot-chart-shell__legend--custom-width]=\"hasSideLegendWidth\"\n [style.--heatmap-pivot-chart-legend-max-width]=\"legendShellMaxWidth\"\n [chart]=\"chartInstance\"\n [series]=\"config.series\"\n [colors]=\"config.colors || deafultColors\"\n [config]=\"config.customLegend || {}\"\n (legendEvent)=\"onLegendEvent($event)\"\n ></lib-chart-legend>\n }\n\n <div\n #chartContainer\n echarts\n [options]=\"chartOption\"\n [theme]=\"theme\"\n (chartInit)=\"onChartInit($event)\"\n class=\"chart-container\"\n ></div>\n\n @if (\n config.customLegend && !config.hideLegend &&\n (config.customLegend.position === 'bottom' || config.customLegend.position === 'right')\n ) {\n <lib-chart-legend\n class=\"heatmap-pivot-chart-shell__legend\"\n [class.heatmap-pivot-chart-shell__legend--custom-width]=\"hasSideLegendWidth\"\n [style.--heatmap-pivot-chart-legend-max-width]=\"legendShellMaxWidth\"\n [chart]=\"chartInstance\"\n [series]=\"config.series\"\n [colors]=\"config.colors || deafultColors\"\n [config]=\"config.customLegend || {}\"\n (legendEvent)=\"onLegendEvent($event)\"\n ></lib-chart-legend>\n }\n</div>\n@if (openCard) {\n <lib-right-click-card\n [cardOptions]=\"contextMenuOptions\"\n (contextMenuClick)=\"onMenuClick($event)\"\n [cardPosition]=\"cardPosition\"\n ></lib-right-click-card>\n}\n", styles: [":host{display:block;height:100%;width:100%;font-family:Arial,sans-serif}.heatmap-pivot-chart-shell{display:flex;flex-direction:column;gap:8px;height:100%;min-height:0;min-width:0;width:100%;padding:10px 0 4px 10px;box-sizing:border-box;background:#fff}.heatmap-pivot-chart-shell--legend-left,.heatmap-pivot-chart-shell--legend-right{flex-direction:row}.heatmap-pivot-chart-shell--legend-bottom{flex-direction:column}.heatmap-pivot-chart-shell__legend{flex:0 0 auto;min-width:0}.heatmap-pivot-chart-shell--legend-left .heatmap-pivot-chart-shell__legend,.heatmap-pivot-chart-shell--legend-right .heatmap-pivot-chart-shell__legend{height:100%;max-width:min(var(--heatmap-pivot-chart-legend-max-width, 360px),70%);min-height:0}.chart-container{width:100%;height:100%;position:relative;flex:1 1 auto;min-height:0;min-width:0}@media (max-width: 640px){.heatmap-pivot-chart-shell,.heatmap-pivot-chart-shell--legend-left,.heatmap-pivot-chart-shell--legend-right{flex-direction:column}.heatmap-pivot-chart-shell--legend-left .heatmap-pivot-chart-shell__legend,.heatmap-pivot-chart-shell--legend-right .heatmap-pivot-chart-shell__legend{flex-basis:auto;max-width:none;width:auto}}\n"], dependencies: [{ kind: "directive", type: NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }, { kind: "component", type: ChartLegendComponent, selector: "lib-chart-legend", inputs: ["chart", "series", "colors", "config", "itemTemplate"], outputs: ["legendEvent"] }, { kind: "component", type: RightClickCard, selector: "lib-right-click-card", inputs: ["cardPosition", "cardOptions"], outputs: ["contextMenuClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5661
+ }
5662
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: HeatmapPivot, decorators: [{
5663
+ type: Component,
5664
+ args: [{ selector: 'lib-heatmap-pivot', standalone: true, imports: [NgxEchartsDirective, ChartLegendComponent, RightClickCard], providers: [provideEchartsCore({ echarts })], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"heatmap-pivot-chart-shell\"\n [class.heatmap-pivot-chart-shell--legend-left]=\"config.customLegend?.position === 'left'\"\n [class.heatmap-pivot-chart-shell--legend-right]=\"config.customLegend?.position === 'right'\"\n [class.heatmap-pivot-chart-shell--legend-bottom]=\"config.customLegend?.position === 'bottom'\"\n>\n @if (\n config.customLegend && !config.hideLegend &&\n config.customLegend.position !== 'bottom' &&\n config.customLegend.position !== 'right'\n ) {\n <lib-chart-legend\n class=\"heatmap-pivot-chart-shell__legend\"\n [class.heatmap-pivot-chart-shell__legend--custom-width]=\"hasSideLegendWidth\"\n [style.--heatmap-pivot-chart-legend-max-width]=\"legendShellMaxWidth\"\n [chart]=\"chartInstance\"\n [series]=\"config.series\"\n [colors]=\"config.colors || deafultColors\"\n [config]=\"config.customLegend || {}\"\n (legendEvent)=\"onLegendEvent($event)\"\n ></lib-chart-legend>\n }\n\n <div\n #chartContainer\n echarts\n [options]=\"chartOption\"\n [theme]=\"theme\"\n (chartInit)=\"onChartInit($event)\"\n class=\"chart-container\"\n ></div>\n\n @if (\n config.customLegend && !config.hideLegend &&\n (config.customLegend.position === 'bottom' || config.customLegend.position === 'right')\n ) {\n <lib-chart-legend\n class=\"heatmap-pivot-chart-shell__legend\"\n [class.heatmap-pivot-chart-shell__legend--custom-width]=\"hasSideLegendWidth\"\n [style.--heatmap-pivot-chart-legend-max-width]=\"legendShellMaxWidth\"\n [chart]=\"chartInstance\"\n [series]=\"config.series\"\n [colors]=\"config.colors || deafultColors\"\n [config]=\"config.customLegend || {}\"\n (legendEvent)=\"onLegendEvent($event)\"\n ></lib-chart-legend>\n }\n</div>\n@if (openCard) {\n <lib-right-click-card\n [cardOptions]=\"contextMenuOptions\"\n (contextMenuClick)=\"onMenuClick($event)\"\n [cardPosition]=\"cardPosition\"\n ></lib-right-click-card>\n}\n", styles: [":host{display:block;height:100%;width:100%;font-family:Arial,sans-serif}.heatmap-pivot-chart-shell{display:flex;flex-direction:column;gap:8px;height:100%;min-height:0;min-width:0;width:100%;padding:10px 0 4px 10px;box-sizing:border-box;background:#fff}.heatmap-pivot-chart-shell--legend-left,.heatmap-pivot-chart-shell--legend-right{flex-direction:row}.heatmap-pivot-chart-shell--legend-bottom{flex-direction:column}.heatmap-pivot-chart-shell__legend{flex:0 0 auto;min-width:0}.heatmap-pivot-chart-shell--legend-left .heatmap-pivot-chart-shell__legend,.heatmap-pivot-chart-shell--legend-right .heatmap-pivot-chart-shell__legend{height:100%;max-width:min(var(--heatmap-pivot-chart-legend-max-width, 360px),70%);min-height:0}.chart-container{width:100%;height:100%;position:relative;flex:1 1 auto;min-height:0;min-width:0}@media (max-width: 640px){.heatmap-pivot-chart-shell,.heatmap-pivot-chart-shell--legend-left,.heatmap-pivot-chart-shell--legend-right{flex-direction:column}.heatmap-pivot-chart-shell--legend-left .heatmap-pivot-chart-shell__legend,.heatmap-pivot-chart-shell--legend-right .heatmap-pivot-chart-shell__legend{flex-basis:auto;max-width:none;width:auto}}\n"] }]
5665
+ }], ctorParameters: () => [{ type: Charts }, { type: i0.ChangeDetectorRef }], propDecorators: { chartContainer: [{
5666
+ type: ViewChild,
5667
+ args: ['chartContainer']
5668
+ }], config: [{
5669
+ type: Input
5670
+ }], theme: [{
5671
+ type: Input
5672
+ }], events: [{
5673
+ type: Input
5674
+ }], contextMenuOptions: [{
5675
+ type: Input
5676
+ }], showContextMenu: [{
5677
+ type: Input
5678
+ }], contextMenuClick: [{
5679
+ type: Output
5680
+ }], chartEvent: [{
5681
+ type: Output
5682
+ }], legendEvent: [{
5683
+ type: Output
5684
+ }], refreshLabelWidth: [{
5685
+ type: HostListener,
5686
+ args: ['window:resize']
5687
+ }], closeCard: [{
5688
+ type: HostListener,
5689
+ args: ['document:click']
5690
+ }] } });
5691
+
4940
5692
  echarts.use([
4941
5693
  ScatterChart,
4942
5694
  GridComponent,
@@ -7560,5 +8312,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
7560
8312
  * Generated bundle index. Do not edit.
7561
8313
  */
7562
8314
 
7563
- export { BarChart, ChartLegendAdapter, ChartLegendComponent, Charts, CustomChartComponent, DoughnutChart, GanttChart, GaugeChart, Heatmap, LinesChart, PieChart, RightClickCard, SankeyChart, ScatterChartComp, StackedAreaChart, StatusTimelineComponent };
8315
+ export { BarChart, ChartLegendAdapter, ChartLegendComponent, Charts, CustomChartComponent, DoughnutChart, GanttChart, GaugeChart, Heatmap, HeatmapPivot, LinesChart, PieChart, RightClickCard, SankeyChart, ScatterChartComp, StackedAreaChart, StatusTimelineComponent };
7564
8316
  //# sourceMappingURL=cats-charts.mjs.map