@gravity-ui/charts 1.7.1 → 1.8.0

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.
@@ -71,7 +71,7 @@ export const ChartInner = (props) => {
71
71
  React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
72
72
  React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotRef: plotRef })))),
73
73
  shapes),
74
- preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip }))),
74
+ preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayerRef.current }))),
75
75
  React.createElement("div", { className: b('html-layer'), ref: htmlLayerRef, style: {
76
76
  '--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
77
77
  } }),
@@ -20,8 +20,10 @@ export declare function useChartInnerProps(props: Props): {
20
20
  top: number;
21
21
  };
22
22
  pagination: {
23
- limit: number;
24
- maxPage: number;
23
+ pages: {
24
+ start: number;
25
+ end: number;
26
+ }[];
25
27
  } | undefined;
26
28
  };
27
29
  legendItems: import("../../hooks").LegendItem[][];
@@ -7,6 +7,7 @@ type Props = {
7
7
  legend: PreparedLegend;
8
8
  items: LegendItem[][];
9
9
  config: LegendConfig;
10
+ htmlLayout: HTMLElement | null;
10
11
  onItemClick: OnLegendItemClick;
11
12
  onUpdate?: () => void;
12
13
  };
@@ -18,27 +18,28 @@ const getLegendPosition = (args) => {
18
18
  return { top, left: offsetWidth + width / 2 - contentWidth / 2 };
19
19
  };
20
20
  const appendPaginator = (args) => {
21
- const { container, offset, maxPage, legend, transform, onArrowClick } = args;
21
+ const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
22
22
  const paginationLine = container.append('g').attr('class', b('pagination'));
23
+ const maxPage = pages.length;
23
24
  let computedWidth = 0;
24
25
  paginationLine
25
26
  .append('text')
26
27
  .text('▲')
27
28
  .attr('class', function () {
28
- return b('pagination-arrow', { inactive: offset === 0 });
29
+ return b('pagination-arrow', { inactive: pageIndex === 0 });
29
30
  })
30
31
  .style('font-size', legend.itemStyle.fontSize)
31
32
  .each(function () {
32
33
  computedWidth += this.getComputedTextLength();
33
34
  })
34
35
  .on('click', function () {
35
- if (offset - 1 >= 0) {
36
- onArrowClick(offset - 1);
36
+ if (pageIndex - 1 >= 0) {
37
+ onArrowClick(pageIndex - 1);
37
38
  }
38
39
  });
39
40
  paginationLine
40
41
  .append('text')
41
- .text(`${offset + 1}/${maxPage}`)
42
+ .text(`${pageIndex + 1}/${maxPage}`)
42
43
  .attr('class', b('pagination-counter'))
43
44
  .attr('x', computedWidth)
44
45
  .style('font-size', legend.itemStyle.fontSize)
@@ -49,13 +50,13 @@ const appendPaginator = (args) => {
49
50
  .append('text')
50
51
  .text('▼')
51
52
  .attr('class', function () {
52
- return b('pagination-arrow', { inactive: offset === maxPage - 1 });
53
+ return b('pagination-arrow', { inactive: pageIndex === maxPage - 1 });
53
54
  })
54
55
  .attr('x', computedWidth)
55
56
  .style('font-size', legend.itemStyle.fontSize)
56
57
  .on('click', function () {
57
- if (offset + 1 < maxPage) {
58
- onArrowClick(offset + 1);
58
+ if (pageIndex + 1 < maxPage) {
59
+ onArrowClick(pageIndex + 1);
59
60
  }
60
61
  });
61
62
  paginationLine.attr('transform', transform);
@@ -64,7 +65,7 @@ const legendSymbolGenerator = lineGenerator()
64
65
  .x((d) => d.x)
65
66
  .y((d) => d.y);
66
67
  function renderLegendSymbol(args) {
67
- const { selection, legend } = args;
68
+ const { selection, legend, legendLineHeight } = args;
68
69
  const line = selection.data();
69
70
  const getXPosition = (i) => {
70
71
  return line.slice(0, i).reduce((acc, legendItem) => {
@@ -82,7 +83,7 @@ function renderLegendSymbol(args) {
82
83
  const color = d.visible ? d.color : '';
83
84
  switch (d.symbol.shape) {
84
85
  case 'path': {
85
- const y = legend.lineHeight / 2;
86
+ const y = legendLineHeight / 2;
86
87
  const points = [
87
88
  { x: x, y },
88
89
  { x: x + d.symbol.width, y },
@@ -100,7 +101,7 @@ function renderLegendSymbol(args) {
100
101
  break;
101
102
  }
102
103
  case 'rect': {
103
- const y = (legend.lineHeight - d.symbol.height) / 2;
104
+ const y = (legendLineHeight - d.symbol.height) / 2;
104
105
  element
105
106
  .append('rect')
106
107
  .attr('x', x)
@@ -113,7 +114,7 @@ function renderLegendSymbol(args) {
113
114
  break;
114
115
  }
115
116
  case 'symbol': {
116
- const y = legend.lineHeight / 2;
117
+ const y = legendLineHeight / 2;
117
118
  const translateX = x + d.symbol.width / 2;
118
119
  element
119
120
  .append('svg:path')
@@ -134,28 +135,36 @@ function renderLegendSymbol(args) {
134
135
  });
135
136
  }
136
137
  export const Legend = (props) => {
137
- const { boundsWidth, chartSeries, legend, items, config, onItemClick, onUpdate } = props;
138
+ const { boundsWidth, chartSeries, legend, items, config, htmlLayout, onItemClick, onUpdate } = props;
138
139
  const ref = React.useRef(null);
139
- const [paginationOffset, setPaginationOffset] = React.useState(0);
140
+ const [pageIndex, setPageIndex] = React.useState(0);
140
141
  React.useEffect(() => {
141
- setPaginationOffset(0);
142
+ setPageIndex(0);
142
143
  }, [boundsWidth]);
143
144
  React.useEffect(() => {
144
- var _a, _b, _c, _d, _e;
145
- if (!ref.current) {
145
+ var _a, _b, _c, _d, _e, _f, _g, _h;
146
+ if (!ref.current || !htmlLayout) {
146
147
  return;
147
148
  }
148
149
  const svgElement = select(ref.current);
149
150
  svgElement.selectAll('*').remove();
151
+ const htmlElement = select(htmlLayout);
152
+ htmlElement.selectAll('[data-legend]').remove();
153
+ const htmlContainer = legend.html
154
+ ? htmlElement.append('div').attr('data-legend', 1).style('position', 'absolute')
155
+ : null;
150
156
  let legendWidth = 0;
151
157
  if (legend.type === 'discrete') {
152
- const limit = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.limit;
153
- const pageItems = typeof limit === 'number'
154
- ? items.slice(paginationOffset * limit, paginationOffset * limit + limit)
158
+ const start = (_b = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.pages[pageIndex]) === null || _b === void 0 ? void 0 : _b.start;
159
+ const end = (_d = (_c = config.pagination) === null || _c === void 0 ? void 0 : _c.pages[pageIndex]) === null || _d === void 0 ? void 0 : _d.end;
160
+ const pageItems = typeof start === 'number' && typeof end === 'number'
161
+ ? items.slice(start, end)
155
162
  : items;
156
- pageItems.forEach((line, lineIndex) => {
163
+ const legendLineHeights = [];
164
+ pageItems.forEach((line) => {
157
165
  var _a;
158
166
  const legendLine = svgElement.append('g').attr('class', b('line'));
167
+ const htmlLegendLine = htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.append('div').style('position', 'absolute');
159
168
  const legendItemTemplate = legendLine
160
169
  .selectAll('legend-history')
161
170
  .data(line)
@@ -175,22 +184,57 @@ export const Legend = (props) => {
175
184
  legend.itemDistance);
176
185
  }, 0);
177
186
  };
178
- renderLegendSymbol({ selection: legendItemTemplate, legend });
179
- legendItemTemplate
180
- .append('text')
181
- .attr('x', function (legendItem, i) {
182
- return (getXPosition(i) + legendItem.symbol.width + legendItem.symbol.padding);
183
- })
184
- .attr('height', legend.lineHeight)
185
- .attr('class', function (d) {
186
- const mods = { selected: d.visible, unselected: !d.visible };
187
- return b('item-text', mods);
188
- })
189
- .html(function (d) {
190
- return ('name' in d && d.name);
191
- })
192
- .style('font-size', legend.itemStyle.fontSize);
193
- const contentWidth = ((_a = legendLine.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0;
187
+ const legendLineHeight = Math.max(...line.map((l) => l.height));
188
+ renderLegendSymbol({ selection: legendItemTemplate, legend, legendLineHeight });
189
+ if (htmlLegendLine) {
190
+ htmlLegendLine
191
+ .selectAll('legend-item')
192
+ .data(line)
193
+ .enter()
194
+ .append('div')
195
+ .attr('class', function (d) {
196
+ const mods = { selected: d.visible, unselected: !d.visible };
197
+ return b('item-text-html', mods);
198
+ })
199
+ .style('font-size', legend.itemStyle.fontSize)
200
+ .style('position', 'absolute')
201
+ .style('left', function (d, i) {
202
+ return `${getXPosition(i) + d.symbol.width + d.symbol.padding}px`;
203
+ })
204
+ .style('top', function (d) {
205
+ if (d.height < legendLineHeight) {
206
+ return `${(legendLineHeight - d.height) / 2}px`;
207
+ }
208
+ return '0px';
209
+ })
210
+ .on('click', function (e, d) {
211
+ onItemClick({ name: d.name, metaKey: e.metaKey });
212
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate();
213
+ })[legend.html ? 'html' : 'text'](function (d) {
214
+ return d.name;
215
+ });
216
+ }
217
+ else {
218
+ legendItemTemplate
219
+ .append('text')
220
+ .attr('x', function (legendItem, i) {
221
+ return (getXPosition(i) +
222
+ legendItem.symbol.width +
223
+ legendItem.symbol.padding);
224
+ })
225
+ .attr('height', legend.height)
226
+ .attr('class', function (d) {
227
+ const mods = { selected: d.visible, unselected: !d.visible };
228
+ return b('item-text', mods);
229
+ })
230
+ .html(function (d) {
231
+ return ('name' in d && d.name);
232
+ })
233
+ .style('font-size', legend.itemStyle.fontSize);
234
+ }
235
+ const contentWidth = (legend.html
236
+ ? getXPosition(line.length) - legend.itemDistance
237
+ : (_a = legendLine.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0;
194
238
  let left = 0;
195
239
  switch (legend.justifyContent) {
196
240
  case 'center': {
@@ -209,27 +253,26 @@ export const Legend = (props) => {
209
253
  break;
210
254
  }
211
255
  }
212
- const top = legend.lineHeight * lineIndex;
256
+ const top = legendLineHeights.reduce((acc, h) => acc + h, 0);
257
+ legendLineHeights.push(legendLineHeight);
213
258
  legendLine.attr('transform', `translate(${[left, top].join(',')})`);
259
+ htmlLegendLine === null || htmlLegendLine === void 0 ? void 0 : htmlLegendLine.style('transform', `translate(${left}px, ${top}px)`);
214
260
  });
215
261
  if (config.pagination) {
216
- const transform = `translate(${[
217
- 0,
218
- legend.lineHeight * config.pagination.limit + legend.lineHeight / 2,
219
- ].join(',')})`;
262
+ const transform = `translate(${[0, legend.height - legend.lineHeight / 2].join(',')})`;
220
263
  appendPaginator({
221
264
  container: svgElement,
222
- offset: paginationOffset,
223
- maxPage: config.pagination.maxPage,
265
+ pageIndex: pageIndex,
224
266
  legend,
225
267
  transform,
226
- onArrowClick: setPaginationOffset,
268
+ pages: config.pagination.pages,
269
+ onArrowClick: setPageIndex,
227
270
  });
228
271
  }
229
272
  }
230
273
  else {
231
274
  // gradient rect
232
- const domain = (_b = legend.colorScale.domain) !== null && _b !== void 0 ? _b : [];
275
+ const domain = (_e = legend.colorScale.domain) !== null && _e !== void 0 ? _e : [];
233
276
  const rectHeight = CONTINUOUS_LEGEND_SIZE.height;
234
277
  svgElement.call(createGradientRect, {
235
278
  y: legend.title.height + legend.title.margin,
@@ -291,9 +334,9 @@ export const Legend = (props) => {
291
334
  .attr('class', b('title'))
292
335
  .append('text')
293
336
  .attr('dx', dx)
294
- .attr('font-weight', (_c = legend.title.style.fontWeight) !== null && _c !== void 0 ? _c : null)
295
- .attr('font-size', (_d = legend.title.style.fontSize) !== null && _d !== void 0 ? _d : null)
296
- .attr('fill', (_e = legend.title.style.fontColor) !== null && _e !== void 0 ? _e : null)
337
+ .attr('font-weight', (_f = legend.title.style.fontWeight) !== null && _f !== void 0 ? _f : null)
338
+ .attr('font-size', (_g = legend.title.style.fontSize) !== null && _g !== void 0 ? _g : null)
339
+ .attr('fill', (_h = legend.title.style.fontColor) !== null && _h !== void 0 ? _h : null)
297
340
  .style('dominant-baseline', 'text-before-edge')
298
341
  .html(legend.title.text);
299
342
  }
@@ -304,6 +347,17 @@ export const Legend = (props) => {
304
347
  contentWidth: legendWidth,
305
348
  });
306
349
  svgElement.attr('transform', `translate(${[left, config.offset.top].join(',')})`);
307
- }, [boundsWidth, chartSeries, onItemClick, onUpdate, legend, items, config, paginationOffset]);
350
+ htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
351
+ }, [
352
+ boundsWidth,
353
+ chartSeries,
354
+ onItemClick,
355
+ onUpdate,
356
+ legend,
357
+ items,
358
+ config,
359
+ pageIndex,
360
+ htmlLayout,
361
+ ]);
308
362
  return React.createElement("g", { className: b(), ref: ref, width: boundsWidth, height: legend.height });
309
363
  };
@@ -27,6 +27,17 @@
27
27
  .gcharts-legend__item-text:hover {
28
28
  fill: var(--g-color-text-complementary);
29
29
  }
30
+ .gcharts-legend__item-text-html {
31
+ cursor: pointer;
32
+ user-select: none;
33
+ white-space: nowrap;
34
+ }
35
+ .gcharts-legend__item-text-html_unselected {
36
+ color: var(--g-color-text-hint);
37
+ }
38
+ .gcharts-legend__item-text-html:hover {
39
+ color: var(--g-color-text-complementary);
40
+ }
30
41
  .gcharts-legend__pagination {
31
42
  user-select: none;
32
43
  fill: var(--g-color-text-primary);
@@ -135,9 +135,12 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
135
135
  });
136
136
  return (React.createElement("div", { key: id, className: b('content-row') },
137
137
  React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
138
- React.createElement("span", null,
139
- seriesData.name || seriesData.id,
140
- "\u00A0"),
138
+ React.createElement("span", { dangerouslySetInnerHTML: {
139
+ __html: [seriesData.name || seriesData.id]
140
+ .flat()
141
+ .join('\n'),
142
+ } }),
143
+ "\u00A0",
141
144
  React.createElement("span", null, formattedValue)));
142
145
  }
143
146
  case 'sankey': {
@@ -18,8 +18,10 @@ export declare const useSeries: (args: Args) => {
18
18
  top: number;
19
19
  };
20
20
  pagination: {
21
- limit: number;
22
- maxPage: number;
21
+ pages: {
22
+ start: number;
23
+ end: number;
24
+ }[];
23
25
  } | undefined;
24
26
  };
25
27
  preparedLegend: import("./types").PreparedLegend;
@@ -1,26 +1,28 @@
1
1
  import type { ChartData } from '../../types';
2
2
  import type { PreparedAxis, PreparedChart } from '../useChartOptions/types';
3
3
  import type { LegendItem, PreparedLegend, PreparedSeries } from './types';
4
- export declare const getPreparedLegend: (args: {
5
- legend: ChartData["legend"];
6
- series: ChartData["series"]["data"];
7
- }) => PreparedLegend;
8
- export declare const getLegendComponents: (args: {
4
+ export declare function getPreparedLegend(args: {
5
+ legend: ChartData['legend'];
6
+ series: ChartData['series']['data'];
7
+ }): PreparedLegend;
8
+ export declare function getLegendComponents(args: {
9
9
  chartWidth: number;
10
10
  chartHeight: number;
11
- chartMargin: PreparedChart["margin"];
11
+ chartMargin: PreparedChart['margin'];
12
12
  series: PreparedSeries[];
13
13
  preparedLegend: PreparedLegend;
14
14
  preparedYAxis: PreparedAxis[];
15
- }) => {
15
+ }): {
16
16
  legendConfig: {
17
17
  offset: {
18
18
  left: number;
19
19
  top: number;
20
20
  };
21
21
  pagination: {
22
- limit: number;
23
- maxPage: number;
22
+ pages: {
23
+ start: number;
24
+ end: number;
25
+ }[];
24
26
  } | undefined;
25
27
  };
26
28
  legendItems: LegendItem[][];
@@ -6,7 +6,7 @@ import { CONTINUOUS_LEGEND_SIZE, legendDefaults } from '../../constants';
6
6
  import { getDefaultColorStops, getDomainForContinuousColorScale, getHorisontalSvgTextHeight, getLabelsSize, } from '../../utils';
7
7
  import { getBoundsWidth } from '../useChartDimensions';
8
8
  import { getYAxisWidth } from '../useChartDimensions/utils';
9
- export const getPreparedLegend = (args) => {
9
+ export function getPreparedLegend(args) {
10
10
  var _a, _b, _c, _d, _e, _f, _g;
11
11
  const { legend, series } = args;
12
12
  const enabled = Boolean(typeof (legend === null || legend === void 0 ? void 0 : legend.enabled) === 'boolean' ? legend === null || legend === void 0 ? void 0 : legend.enabled : series.length > 1);
@@ -69,30 +69,38 @@ export const getPreparedLegend = (args) => {
69
69
  width: legendWidth,
70
70
  ticks,
71
71
  colorScale,
72
+ html: get(legend, 'html', false),
72
73
  };
73
- };
74
- const getFlattenLegendItems = (series) => {
74
+ }
75
+ function getFlattenLegendItems(series, preparedLegend) {
75
76
  return series.reduce((acc, s) => {
76
77
  const legendEnabled = get(s, 'legend.enabled', true);
77
78
  if (legendEnabled) {
78
- acc.push(Object.assign(Object.assign({}, s), { symbol: s.legend.symbol }));
79
+ acc.push(Object.assign(Object.assign({}, s), { height: preparedLegend.lineHeight, symbol: s.legend.symbol }));
79
80
  }
80
81
  return acc;
81
82
  }, []);
82
- };
83
- const getGroupedLegendItems = (args) => {
83
+ }
84
+ function getGroupedLegendItems(args) {
84
85
  const { maxLegendWidth, items, preparedLegend } = args;
85
86
  const result = [[]];
87
+ const bodySelection = select(document.body);
86
88
  let textWidthsInLine = [0];
87
89
  let lineIndex = 0;
88
90
  items.forEach((item) => {
89
- select(document.body)
90
- .append('text')
91
- .text(item.name)
91
+ const itemSelection = preparedLegend.html
92
+ ? bodySelection
93
+ .append('div')
94
+ .html(item.name)
95
+ .style('position', 'absolute')
96
+ .style('display', 'inline-block')
97
+ : bodySelection.append('text').text(item.name);
98
+ itemSelection
92
99
  .style('font-size', preparedLegend.itemStyle.fontSize)
93
100
  .each(function () {
94
101
  const resultItem = clone(item);
95
- const textWidth = this.getBoundingClientRect().width;
102
+ const { height, width: textWidth } = this.getBoundingClientRect();
103
+ resultItem.height = height;
96
104
  resultItem.textWidth = textWidth;
97
105
  textWidthsInLine.push(textWidth);
98
106
  const textsWidth = textWidthsInLine.reduce((acc, width) => acc + width, 0);
@@ -114,12 +122,36 @@ const getGroupedLegendItems = (args) => {
114
122
  .remove();
115
123
  });
116
124
  return result;
117
- };
118
- export const getLegendComponents = (args) => {
125
+ }
126
+ function getPagination(args) {
127
+ const { items, maxLegendHeight, paginatorHeight } = args;
128
+ const pages = [];
129
+ let currentPageIndex = 0;
130
+ let currentHeight = 0;
131
+ items.forEach((item, i) => {
132
+ if (!pages[currentPageIndex]) {
133
+ pages[currentPageIndex] = { start: i, end: i };
134
+ }
135
+ const legendLineHeight = Math.max(...item.map((item) => item.height));
136
+ currentHeight += legendLineHeight;
137
+ if (currentHeight > maxLegendHeight - paginatorHeight) {
138
+ pages[currentPageIndex].end = i;
139
+ currentPageIndex += 1;
140
+ currentHeight = legendLineHeight;
141
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice#end
142
+ pages[currentPageIndex] = { start: i, end: i + (i === items.length - 1 ? 1 : 0) };
143
+ }
144
+ else if (i === items.length - 1) {
145
+ pages[currentPageIndex].end = i + 1;
146
+ }
147
+ });
148
+ return { pages };
149
+ }
150
+ export function getLegendComponents(args) {
119
151
  const { chartWidth, chartHeight, chartMargin, series, preparedLegend, preparedYAxis } = args;
120
152
  const maxLegendWidth = getBoundsWidth({ chartWidth, chartMargin, preparedYAxis });
121
153
  const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
122
- const flattenLegendItems = getFlattenLegendItems(series);
154
+ const flattenLegendItems = getFlattenLegendItems(series, preparedLegend);
123
155
  const items = getGroupedLegendItems({
124
156
  maxLegendWidth,
125
157
  items: flattenLegendItems,
@@ -127,13 +159,19 @@ export const getLegendComponents = (args) => {
127
159
  });
128
160
  let pagination;
129
161
  if (preparedLegend.type === 'discrete') {
130
- let legendHeight = preparedLegend.lineHeight * items.length;
162
+ const lineHeights = items.reduce((acc, item) => {
163
+ acc.push(Math.max(...item.map((item) => item.height)));
164
+ return acc;
165
+ }, []);
166
+ let legendHeight = lineHeights.reduce((acc, height) => acc + height, 0);
131
167
  if (maxLegendHeight < legendHeight) {
132
- // extra line for paginator
133
- const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight) - 1;
134
- const maxPage = Math.ceil(items.length / limit);
135
- pagination = { limit, maxPage };
136
- legendHeight = preparedLegend.lineHeight * (limit + 1);
168
+ const lines = Math.floor(maxLegendHeight / preparedLegend.lineHeight);
169
+ legendHeight = preparedLegend.lineHeight * lines;
170
+ pagination = getPagination({
171
+ items,
172
+ maxLegendHeight: legendHeight,
173
+ paginatorHeight: preparedLegend.lineHeight,
174
+ });
137
175
  }
138
176
  preparedLegend.height = legendHeight;
139
177
  }
@@ -143,4 +181,4 @@ export const getLegendComponents = (args) => {
143
181
  top,
144
182
  };
145
183
  return { legendConfig: { offset, pagination }, legendItems: items };
146
- };
184
+ }
@@ -39,6 +39,7 @@ export type OnLegendItemClick = (data: {
39
39
  }) => void;
40
40
  export type LegendItem = {
41
41
  color: string;
42
+ height: number;
42
43
  name: string;
43
44
  symbol: PreparedLegendSymbol;
44
45
  textWidth: number;
@@ -51,8 +52,10 @@ export type LegendConfig = {
51
52
  top: number;
52
53
  };
53
54
  pagination?: {
54
- limit: number;
55
- maxPage: number;
55
+ pages: {
56
+ start: number;
57
+ end: number;
58
+ }[];
56
59
  };
57
60
  };
58
61
  export type PreparedHaloOptions = {
@@ -53,6 +53,12 @@ export interface ChartLegend {
53
53
  domain?: number[];
54
54
  };
55
55
  width?: number;
56
+ /**
57
+ * Allows to use any html-tags to display the content.
58
+ *
59
+ * @default false
60
+ * */
61
+ html?: boolean;
56
62
  }
57
63
  export interface BaseLegendSymbol {
58
64
  /**
@@ -71,7 +71,7 @@ export const ChartInner = (props) => {
71
71
  React.createElement("g", { transform: `translate(0, ${boundsHeight})` },
72
72
  React.createElement(AxisX, { leftmostLimit: svgXPos, axis: xAxis, width: boundsWidth, height: boundsHeight, scale: xScale, split: preparedSplit, plotRef: plotRef })))),
73
73
  shapes),
74
- preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip }))),
74
+ preparedLegend.enabled && (React.createElement(Legend, { chartSeries: preparedSeries, boundsWidth: boundsWidth, legend: preparedLegend, items: legendItems, config: legendConfig, onItemClick: handleLegendItemClick, onUpdate: unpinTooltip, htmlLayout: htmlLayerRef.current }))),
75
75
  React.createElement("div", { className: b('html-layer'), ref: htmlLayerRef, style: {
76
76
  '--g-html-layout-transform': `translate(${boundsOffsetLeft}px, ${boundsOffsetTop}px)`,
77
77
  } }),
@@ -20,8 +20,10 @@ export declare function useChartInnerProps(props: Props): {
20
20
  top: number;
21
21
  };
22
22
  pagination: {
23
- limit: number;
24
- maxPage: number;
23
+ pages: {
24
+ start: number;
25
+ end: number;
26
+ }[];
25
27
  } | undefined;
26
28
  };
27
29
  legendItems: import("../../hooks").LegendItem[][];
@@ -7,6 +7,7 @@ type Props = {
7
7
  legend: PreparedLegend;
8
8
  items: LegendItem[][];
9
9
  config: LegendConfig;
10
+ htmlLayout: HTMLElement | null;
10
11
  onItemClick: OnLegendItemClick;
11
12
  onUpdate?: () => void;
12
13
  };
@@ -18,27 +18,28 @@ const getLegendPosition = (args) => {
18
18
  return { top, left: offsetWidth + width / 2 - contentWidth / 2 };
19
19
  };
20
20
  const appendPaginator = (args) => {
21
- const { container, offset, maxPage, legend, transform, onArrowClick } = args;
21
+ const { container, pageIndex, legend, transform, pages, onArrowClick } = args;
22
22
  const paginationLine = container.append('g').attr('class', b('pagination'));
23
+ const maxPage = pages.length;
23
24
  let computedWidth = 0;
24
25
  paginationLine
25
26
  .append('text')
26
27
  .text('▲')
27
28
  .attr('class', function () {
28
- return b('pagination-arrow', { inactive: offset === 0 });
29
+ return b('pagination-arrow', { inactive: pageIndex === 0 });
29
30
  })
30
31
  .style('font-size', legend.itemStyle.fontSize)
31
32
  .each(function () {
32
33
  computedWidth += this.getComputedTextLength();
33
34
  })
34
35
  .on('click', function () {
35
- if (offset - 1 >= 0) {
36
- onArrowClick(offset - 1);
36
+ if (pageIndex - 1 >= 0) {
37
+ onArrowClick(pageIndex - 1);
37
38
  }
38
39
  });
39
40
  paginationLine
40
41
  .append('text')
41
- .text(`${offset + 1}/${maxPage}`)
42
+ .text(`${pageIndex + 1}/${maxPage}`)
42
43
  .attr('class', b('pagination-counter'))
43
44
  .attr('x', computedWidth)
44
45
  .style('font-size', legend.itemStyle.fontSize)
@@ -49,13 +50,13 @@ const appendPaginator = (args) => {
49
50
  .append('text')
50
51
  .text('▼')
51
52
  .attr('class', function () {
52
- return b('pagination-arrow', { inactive: offset === maxPage - 1 });
53
+ return b('pagination-arrow', { inactive: pageIndex === maxPage - 1 });
53
54
  })
54
55
  .attr('x', computedWidth)
55
56
  .style('font-size', legend.itemStyle.fontSize)
56
57
  .on('click', function () {
57
- if (offset + 1 < maxPage) {
58
- onArrowClick(offset + 1);
58
+ if (pageIndex + 1 < maxPage) {
59
+ onArrowClick(pageIndex + 1);
59
60
  }
60
61
  });
61
62
  paginationLine.attr('transform', transform);
@@ -64,7 +65,7 @@ const legendSymbolGenerator = lineGenerator()
64
65
  .x((d) => d.x)
65
66
  .y((d) => d.y);
66
67
  function renderLegendSymbol(args) {
67
- const { selection, legend } = args;
68
+ const { selection, legend, legendLineHeight } = args;
68
69
  const line = selection.data();
69
70
  const getXPosition = (i) => {
70
71
  return line.slice(0, i).reduce((acc, legendItem) => {
@@ -82,7 +83,7 @@ function renderLegendSymbol(args) {
82
83
  const color = d.visible ? d.color : '';
83
84
  switch (d.symbol.shape) {
84
85
  case 'path': {
85
- const y = legend.lineHeight / 2;
86
+ const y = legendLineHeight / 2;
86
87
  const points = [
87
88
  { x: x, y },
88
89
  { x: x + d.symbol.width, y },
@@ -100,7 +101,7 @@ function renderLegendSymbol(args) {
100
101
  break;
101
102
  }
102
103
  case 'rect': {
103
- const y = (legend.lineHeight - d.symbol.height) / 2;
104
+ const y = (legendLineHeight - d.symbol.height) / 2;
104
105
  element
105
106
  .append('rect')
106
107
  .attr('x', x)
@@ -113,7 +114,7 @@ function renderLegendSymbol(args) {
113
114
  break;
114
115
  }
115
116
  case 'symbol': {
116
- const y = legend.lineHeight / 2;
117
+ const y = legendLineHeight / 2;
117
118
  const translateX = x + d.symbol.width / 2;
118
119
  element
119
120
  .append('svg:path')
@@ -134,28 +135,36 @@ function renderLegendSymbol(args) {
134
135
  });
135
136
  }
136
137
  export const Legend = (props) => {
137
- const { boundsWidth, chartSeries, legend, items, config, onItemClick, onUpdate } = props;
138
+ const { boundsWidth, chartSeries, legend, items, config, htmlLayout, onItemClick, onUpdate } = props;
138
139
  const ref = React.useRef(null);
139
- const [paginationOffset, setPaginationOffset] = React.useState(0);
140
+ const [pageIndex, setPageIndex] = React.useState(0);
140
141
  React.useEffect(() => {
141
- setPaginationOffset(0);
142
+ setPageIndex(0);
142
143
  }, [boundsWidth]);
143
144
  React.useEffect(() => {
144
- var _a, _b, _c, _d, _e;
145
- if (!ref.current) {
145
+ var _a, _b, _c, _d, _e, _f, _g, _h;
146
+ if (!ref.current || !htmlLayout) {
146
147
  return;
147
148
  }
148
149
  const svgElement = select(ref.current);
149
150
  svgElement.selectAll('*').remove();
151
+ const htmlElement = select(htmlLayout);
152
+ htmlElement.selectAll('[data-legend]').remove();
153
+ const htmlContainer = legend.html
154
+ ? htmlElement.append('div').attr('data-legend', 1).style('position', 'absolute')
155
+ : null;
150
156
  let legendWidth = 0;
151
157
  if (legend.type === 'discrete') {
152
- const limit = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.limit;
153
- const pageItems = typeof limit === 'number'
154
- ? items.slice(paginationOffset * limit, paginationOffset * limit + limit)
158
+ const start = (_b = (_a = config.pagination) === null || _a === void 0 ? void 0 : _a.pages[pageIndex]) === null || _b === void 0 ? void 0 : _b.start;
159
+ const end = (_d = (_c = config.pagination) === null || _c === void 0 ? void 0 : _c.pages[pageIndex]) === null || _d === void 0 ? void 0 : _d.end;
160
+ const pageItems = typeof start === 'number' && typeof end === 'number'
161
+ ? items.slice(start, end)
155
162
  : items;
156
- pageItems.forEach((line, lineIndex) => {
163
+ const legendLineHeights = [];
164
+ pageItems.forEach((line) => {
157
165
  var _a;
158
166
  const legendLine = svgElement.append('g').attr('class', b('line'));
167
+ const htmlLegendLine = htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.append('div').style('position', 'absolute');
159
168
  const legendItemTemplate = legendLine
160
169
  .selectAll('legend-history')
161
170
  .data(line)
@@ -175,22 +184,57 @@ export const Legend = (props) => {
175
184
  legend.itemDistance);
176
185
  }, 0);
177
186
  };
178
- renderLegendSymbol({ selection: legendItemTemplate, legend });
179
- legendItemTemplate
180
- .append('text')
181
- .attr('x', function (legendItem, i) {
182
- return (getXPosition(i) + legendItem.symbol.width + legendItem.symbol.padding);
183
- })
184
- .attr('height', legend.lineHeight)
185
- .attr('class', function (d) {
186
- const mods = { selected: d.visible, unselected: !d.visible };
187
- return b('item-text', mods);
188
- })
189
- .html(function (d) {
190
- return ('name' in d && d.name);
191
- })
192
- .style('font-size', legend.itemStyle.fontSize);
193
- const contentWidth = ((_a = legendLine.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0;
187
+ const legendLineHeight = Math.max(...line.map((l) => l.height));
188
+ renderLegendSymbol({ selection: legendItemTemplate, legend, legendLineHeight });
189
+ if (htmlLegendLine) {
190
+ htmlLegendLine
191
+ .selectAll('legend-item')
192
+ .data(line)
193
+ .enter()
194
+ .append('div')
195
+ .attr('class', function (d) {
196
+ const mods = { selected: d.visible, unselected: !d.visible };
197
+ return b('item-text-html', mods);
198
+ })
199
+ .style('font-size', legend.itemStyle.fontSize)
200
+ .style('position', 'absolute')
201
+ .style('left', function (d, i) {
202
+ return `${getXPosition(i) + d.symbol.width + d.symbol.padding}px`;
203
+ })
204
+ .style('top', function (d) {
205
+ if (d.height < legendLineHeight) {
206
+ return `${(legendLineHeight - d.height) / 2}px`;
207
+ }
208
+ return '0px';
209
+ })
210
+ .on('click', function (e, d) {
211
+ onItemClick({ name: d.name, metaKey: e.metaKey });
212
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate();
213
+ })[legend.html ? 'html' : 'text'](function (d) {
214
+ return d.name;
215
+ });
216
+ }
217
+ else {
218
+ legendItemTemplate
219
+ .append('text')
220
+ .attr('x', function (legendItem, i) {
221
+ return (getXPosition(i) +
222
+ legendItem.symbol.width +
223
+ legendItem.symbol.padding);
224
+ })
225
+ .attr('height', legend.height)
226
+ .attr('class', function (d) {
227
+ const mods = { selected: d.visible, unselected: !d.visible };
228
+ return b('item-text', mods);
229
+ })
230
+ .html(function (d) {
231
+ return ('name' in d && d.name);
232
+ })
233
+ .style('font-size', legend.itemStyle.fontSize);
234
+ }
235
+ const contentWidth = (legend.html
236
+ ? getXPosition(line.length) - legend.itemDistance
237
+ : (_a = legendLine.node()) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width) || 0;
194
238
  let left = 0;
195
239
  switch (legend.justifyContent) {
196
240
  case 'center': {
@@ -209,27 +253,26 @@ export const Legend = (props) => {
209
253
  break;
210
254
  }
211
255
  }
212
- const top = legend.lineHeight * lineIndex;
256
+ const top = legendLineHeights.reduce((acc, h) => acc + h, 0);
257
+ legendLineHeights.push(legendLineHeight);
213
258
  legendLine.attr('transform', `translate(${[left, top].join(',')})`);
259
+ htmlLegendLine === null || htmlLegendLine === void 0 ? void 0 : htmlLegendLine.style('transform', `translate(${left}px, ${top}px)`);
214
260
  });
215
261
  if (config.pagination) {
216
- const transform = `translate(${[
217
- 0,
218
- legend.lineHeight * config.pagination.limit + legend.lineHeight / 2,
219
- ].join(',')})`;
262
+ const transform = `translate(${[0, legend.height - legend.lineHeight / 2].join(',')})`;
220
263
  appendPaginator({
221
264
  container: svgElement,
222
- offset: paginationOffset,
223
- maxPage: config.pagination.maxPage,
265
+ pageIndex: pageIndex,
224
266
  legend,
225
267
  transform,
226
- onArrowClick: setPaginationOffset,
268
+ pages: config.pagination.pages,
269
+ onArrowClick: setPageIndex,
227
270
  });
228
271
  }
229
272
  }
230
273
  else {
231
274
  // gradient rect
232
- const domain = (_b = legend.colorScale.domain) !== null && _b !== void 0 ? _b : [];
275
+ const domain = (_e = legend.colorScale.domain) !== null && _e !== void 0 ? _e : [];
233
276
  const rectHeight = CONTINUOUS_LEGEND_SIZE.height;
234
277
  svgElement.call(createGradientRect, {
235
278
  y: legend.title.height + legend.title.margin,
@@ -291,9 +334,9 @@ export const Legend = (props) => {
291
334
  .attr('class', b('title'))
292
335
  .append('text')
293
336
  .attr('dx', dx)
294
- .attr('font-weight', (_c = legend.title.style.fontWeight) !== null && _c !== void 0 ? _c : null)
295
- .attr('font-size', (_d = legend.title.style.fontSize) !== null && _d !== void 0 ? _d : null)
296
- .attr('fill', (_e = legend.title.style.fontColor) !== null && _e !== void 0 ? _e : null)
337
+ .attr('font-weight', (_f = legend.title.style.fontWeight) !== null && _f !== void 0 ? _f : null)
338
+ .attr('font-size', (_g = legend.title.style.fontSize) !== null && _g !== void 0 ? _g : null)
339
+ .attr('fill', (_h = legend.title.style.fontColor) !== null && _h !== void 0 ? _h : null)
297
340
  .style('dominant-baseline', 'text-before-edge')
298
341
  .html(legend.title.text);
299
342
  }
@@ -304,6 +347,17 @@ export const Legend = (props) => {
304
347
  contentWidth: legendWidth,
305
348
  });
306
349
  svgElement.attr('transform', `translate(${[left, config.offset.top].join(',')})`);
307
- }, [boundsWidth, chartSeries, onItemClick, onUpdate, legend, items, config, paginationOffset]);
350
+ htmlContainer === null || htmlContainer === void 0 ? void 0 : htmlContainer.style('transform', `translate(${left}px, ${config.offset.top}px)`);
351
+ }, [
352
+ boundsWidth,
353
+ chartSeries,
354
+ onItemClick,
355
+ onUpdate,
356
+ legend,
357
+ items,
358
+ config,
359
+ pageIndex,
360
+ htmlLayout,
361
+ ]);
308
362
  return React.createElement("g", { className: b(), ref: ref, width: boundsWidth, height: legend.height });
309
363
  };
@@ -27,6 +27,17 @@
27
27
  .gcharts-legend__item-text:hover {
28
28
  fill: var(--g-color-text-complementary);
29
29
  }
30
+ .gcharts-legend__item-text-html {
31
+ cursor: pointer;
32
+ user-select: none;
33
+ white-space: nowrap;
34
+ }
35
+ .gcharts-legend__item-text-html_unselected {
36
+ color: var(--g-color-text-hint);
37
+ }
38
+ .gcharts-legend__item-text-html:hover {
39
+ color: var(--g-color-text-complementary);
40
+ }
30
41
  .gcharts-legend__pagination {
31
42
  user-select: none;
32
43
  fill: var(--g-color-text-primary);
@@ -135,9 +135,12 @@ export const DefaultContent = ({ hovered, xAxis, yAxis, valueFormat }) => {
135
135
  });
136
136
  return (React.createElement("div", { key: id, className: b('content-row') },
137
137
  React.createElement("div", { className: b('color'), style: { backgroundColor: color } }),
138
- React.createElement("span", null,
139
- seriesData.name || seriesData.id,
140
- "\u00A0"),
138
+ React.createElement("span", { dangerouslySetInnerHTML: {
139
+ __html: [seriesData.name || seriesData.id]
140
+ .flat()
141
+ .join('\n'),
142
+ } }),
143
+ "\u00A0",
141
144
  React.createElement("span", null, formattedValue)));
142
145
  }
143
146
  case 'sankey': {
@@ -18,8 +18,10 @@ export declare const useSeries: (args: Args) => {
18
18
  top: number;
19
19
  };
20
20
  pagination: {
21
- limit: number;
22
- maxPage: number;
21
+ pages: {
22
+ start: number;
23
+ end: number;
24
+ }[];
23
25
  } | undefined;
24
26
  };
25
27
  preparedLegend: import("./types").PreparedLegend;
@@ -1,26 +1,28 @@
1
1
  import type { ChartData } from '../../types';
2
2
  import type { PreparedAxis, PreparedChart } from '../useChartOptions/types';
3
3
  import type { LegendItem, PreparedLegend, PreparedSeries } from './types';
4
- export declare const getPreparedLegend: (args: {
5
- legend: ChartData["legend"];
6
- series: ChartData["series"]["data"];
7
- }) => PreparedLegend;
8
- export declare const getLegendComponents: (args: {
4
+ export declare function getPreparedLegend(args: {
5
+ legend: ChartData['legend'];
6
+ series: ChartData['series']['data'];
7
+ }): PreparedLegend;
8
+ export declare function getLegendComponents(args: {
9
9
  chartWidth: number;
10
10
  chartHeight: number;
11
- chartMargin: PreparedChart["margin"];
11
+ chartMargin: PreparedChart['margin'];
12
12
  series: PreparedSeries[];
13
13
  preparedLegend: PreparedLegend;
14
14
  preparedYAxis: PreparedAxis[];
15
- }) => {
15
+ }): {
16
16
  legendConfig: {
17
17
  offset: {
18
18
  left: number;
19
19
  top: number;
20
20
  };
21
21
  pagination: {
22
- limit: number;
23
- maxPage: number;
22
+ pages: {
23
+ start: number;
24
+ end: number;
25
+ }[];
24
26
  } | undefined;
25
27
  };
26
28
  legendItems: LegendItem[][];
@@ -6,7 +6,7 @@ import { CONTINUOUS_LEGEND_SIZE, legendDefaults } from '../../constants';
6
6
  import { getDefaultColorStops, getDomainForContinuousColorScale, getHorisontalSvgTextHeight, getLabelsSize, } from '../../utils';
7
7
  import { getBoundsWidth } from '../useChartDimensions';
8
8
  import { getYAxisWidth } from '../useChartDimensions/utils';
9
- export const getPreparedLegend = (args) => {
9
+ export function getPreparedLegend(args) {
10
10
  var _a, _b, _c, _d, _e, _f, _g;
11
11
  const { legend, series } = args;
12
12
  const enabled = Boolean(typeof (legend === null || legend === void 0 ? void 0 : legend.enabled) === 'boolean' ? legend === null || legend === void 0 ? void 0 : legend.enabled : series.length > 1);
@@ -69,30 +69,38 @@ export const getPreparedLegend = (args) => {
69
69
  width: legendWidth,
70
70
  ticks,
71
71
  colorScale,
72
+ html: get(legend, 'html', false),
72
73
  };
73
- };
74
- const getFlattenLegendItems = (series) => {
74
+ }
75
+ function getFlattenLegendItems(series, preparedLegend) {
75
76
  return series.reduce((acc, s) => {
76
77
  const legendEnabled = get(s, 'legend.enabled', true);
77
78
  if (legendEnabled) {
78
- acc.push(Object.assign(Object.assign({}, s), { symbol: s.legend.symbol }));
79
+ acc.push(Object.assign(Object.assign({}, s), { height: preparedLegend.lineHeight, symbol: s.legend.symbol }));
79
80
  }
80
81
  return acc;
81
82
  }, []);
82
- };
83
- const getGroupedLegendItems = (args) => {
83
+ }
84
+ function getGroupedLegendItems(args) {
84
85
  const { maxLegendWidth, items, preparedLegend } = args;
85
86
  const result = [[]];
87
+ const bodySelection = select(document.body);
86
88
  let textWidthsInLine = [0];
87
89
  let lineIndex = 0;
88
90
  items.forEach((item) => {
89
- select(document.body)
90
- .append('text')
91
- .text(item.name)
91
+ const itemSelection = preparedLegend.html
92
+ ? bodySelection
93
+ .append('div')
94
+ .html(item.name)
95
+ .style('position', 'absolute')
96
+ .style('display', 'inline-block')
97
+ : bodySelection.append('text').text(item.name);
98
+ itemSelection
92
99
  .style('font-size', preparedLegend.itemStyle.fontSize)
93
100
  .each(function () {
94
101
  const resultItem = clone(item);
95
- const textWidth = this.getBoundingClientRect().width;
102
+ const { height, width: textWidth } = this.getBoundingClientRect();
103
+ resultItem.height = height;
96
104
  resultItem.textWidth = textWidth;
97
105
  textWidthsInLine.push(textWidth);
98
106
  const textsWidth = textWidthsInLine.reduce((acc, width) => acc + width, 0);
@@ -114,12 +122,36 @@ const getGroupedLegendItems = (args) => {
114
122
  .remove();
115
123
  });
116
124
  return result;
117
- };
118
- export const getLegendComponents = (args) => {
125
+ }
126
+ function getPagination(args) {
127
+ const { items, maxLegendHeight, paginatorHeight } = args;
128
+ const pages = [];
129
+ let currentPageIndex = 0;
130
+ let currentHeight = 0;
131
+ items.forEach((item, i) => {
132
+ if (!pages[currentPageIndex]) {
133
+ pages[currentPageIndex] = { start: i, end: i };
134
+ }
135
+ const legendLineHeight = Math.max(...item.map((item) => item.height));
136
+ currentHeight += legendLineHeight;
137
+ if (currentHeight > maxLegendHeight - paginatorHeight) {
138
+ pages[currentPageIndex].end = i;
139
+ currentPageIndex += 1;
140
+ currentHeight = legendLineHeight;
141
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice#end
142
+ pages[currentPageIndex] = { start: i, end: i + (i === items.length - 1 ? 1 : 0) };
143
+ }
144
+ else if (i === items.length - 1) {
145
+ pages[currentPageIndex].end = i + 1;
146
+ }
147
+ });
148
+ return { pages };
149
+ }
150
+ export function getLegendComponents(args) {
119
151
  const { chartWidth, chartHeight, chartMargin, series, preparedLegend, preparedYAxis } = args;
120
152
  const maxLegendWidth = getBoundsWidth({ chartWidth, chartMargin, preparedYAxis });
121
153
  const maxLegendHeight = (chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.margin) / 2;
122
- const flattenLegendItems = getFlattenLegendItems(series);
154
+ const flattenLegendItems = getFlattenLegendItems(series, preparedLegend);
123
155
  const items = getGroupedLegendItems({
124
156
  maxLegendWidth,
125
157
  items: flattenLegendItems,
@@ -127,13 +159,19 @@ export const getLegendComponents = (args) => {
127
159
  });
128
160
  let pagination;
129
161
  if (preparedLegend.type === 'discrete') {
130
- let legendHeight = preparedLegend.lineHeight * items.length;
162
+ const lineHeights = items.reduce((acc, item) => {
163
+ acc.push(Math.max(...item.map((item) => item.height)));
164
+ return acc;
165
+ }, []);
166
+ let legendHeight = lineHeights.reduce((acc, height) => acc + height, 0);
131
167
  if (maxLegendHeight < legendHeight) {
132
- // extra line for paginator
133
- const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight) - 1;
134
- const maxPage = Math.ceil(items.length / limit);
135
- pagination = { limit, maxPage };
136
- legendHeight = preparedLegend.lineHeight * (limit + 1);
168
+ const lines = Math.floor(maxLegendHeight / preparedLegend.lineHeight);
169
+ legendHeight = preparedLegend.lineHeight * lines;
170
+ pagination = getPagination({
171
+ items,
172
+ maxLegendHeight: legendHeight,
173
+ paginatorHeight: preparedLegend.lineHeight,
174
+ });
137
175
  }
138
176
  preparedLegend.height = legendHeight;
139
177
  }
@@ -143,4 +181,4 @@ export const getLegendComponents = (args) => {
143
181
  top,
144
182
  };
145
183
  return { legendConfig: { offset, pagination }, legendItems: items };
146
- };
184
+ }
@@ -39,6 +39,7 @@ export type OnLegendItemClick = (data: {
39
39
  }) => void;
40
40
  export type LegendItem = {
41
41
  color: string;
42
+ height: number;
42
43
  name: string;
43
44
  symbol: PreparedLegendSymbol;
44
45
  textWidth: number;
@@ -51,8 +52,10 @@ export type LegendConfig = {
51
52
  top: number;
52
53
  };
53
54
  pagination?: {
54
- limit: number;
55
- maxPage: number;
55
+ pages: {
56
+ start: number;
57
+ end: number;
58
+ }[];
56
59
  };
57
60
  };
58
61
  export type PreparedHaloOptions = {
@@ -53,6 +53,12 @@ export interface ChartLegend {
53
53
  domain?: number[];
54
54
  };
55
55
  width?: number;
56
+ /**
57
+ * Allows to use any html-tags to display the content.
58
+ *
59
+ * @default false
60
+ * */
61
+ html?: boolean;
56
62
  }
57
63
  export interface BaseLegendSymbol {
58
64
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/charts",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "React component used to render charts",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",