diff --git a/.size-limit.json b/.size-limit.json index 58a7a0fc..41a7e858 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -7,7 +7,7 @@ }, { "path": "dist/index.cjs", - "limit": "7.3 kB", + "limit": "7.35 kB", "import": "{ BarChart }" }, { diff --git a/src/charts/BarChart/BarChart.spec.ts b/src/charts/BarChart/BarChart.spec.ts index 4330f40e..d3ce6070 100644 --- a/src/charts/BarChart/BarChart.spec.ts +++ b/src/charts/BarChart/BarChart.spec.ts @@ -399,5 +399,33 @@ describe('Charts', () => { ).toBeDefined(); }); }); + + it('should correct apply class names', async () => { + data = { + labels: ['A', 'B', 'C'], + series: [ + { + className: 'series-1', + data: [1, 2, 3] + }, + { + className: 'series-2', + data: [4, 5, 6] + } + ] + }; + options = { + reverseData: true + }; + await createChart(); + + const seriesElements = document.querySelectorAll('.ct-series'); + + expect(seriesElements[0]).toHaveClass('series-2'); + expect(seriesElements[0]).toContainHTML('ct:value="6"'); + + expect(seriesElements[1]).toHaveClass('series-1'); + expect(seriesElements[1]).toContainHTML('ct:value="3"'); + }); }); }); diff --git a/src/charts/BarChart/BarChart.ts b/src/charts/BarChart/BarChart.ts index c519589c..db78dcdb 100644 --- a/src/charts/BarChart/BarChart.ts +++ b/src/charts/BarChart/BarChart.ts @@ -7,13 +7,18 @@ import type { BarDrawEvent, BarChartEventsTypes } from './BarChart.types'; -import type { NormalizedSeries, ResponsiveOptions } from '../../core'; +import type { + NormalizedSeries, + ResponsiveOptions, + AllSeriesTypes +} from '../../core'; import { isNumeric, noop, serialMap, extend, - safeHasProperty + safeHasProperty, + each } from '../../utils'; import { alphaNumerate, @@ -365,215 +370,221 @@ export class BarChart extends BaseChart { } // Draw the series - data.series.forEach((series, seriesIndex) => { - // Calculating bi-polar value of index for seriesOffset. For i = 0..4 biPol will be -1.5, -0.5, 0.5, 1.5 etc. - const biPol = seriesIndex - (data.series.length - 1) / 2; - // Half of the period width between vertical grid lines used to position bars - let periodHalfLength: number; - - // We need to set periodHalfLength based on some options combinations - if (options.distributeSeries && !options.stackBars) { - // If distributed series are enabled but stacked bars aren't, we need to use the length of the normaizedData array - // which is the series count and divide by 2 - periodHalfLength = - labelAxis.axisLength / normalizedData.series.length / 2; - } else if (options.distributeSeries && options.stackBars) { - // If distributed series and stacked bars are enabled we'll only get one bar so we should just divide the axis - // length by 2 - periodHalfLength = labelAxis.axisLength / 2; - } else { - // On regular bar charts we should just use the series length - periodHalfLength = - labelAxis.axisLength / normalizedData.series[seriesIndex].length / 2; - } - - // Adding the series group to the series element - const seriesElement = seriesGroup.elem('g'); - const seriesName = safeHasProperty(series, 'name') && series.name; - const seriesClassName = - safeHasProperty(series, 'className') && series.className; - const seriesMeta = safeHasProperty(series, 'meta') - ? series.meta - : undefined; - - // Write attributes to series group element. If series name or meta is undefined the attributes will not be written - if (seriesName) { - seriesElement.attr({ - 'ct:series-name': seriesName - }); - } - - if (seriesMeta) { - seriesElement.attr({ - 'ct:meta': serialize(seriesMeta) - }); - } - - // Use series class from series data or if not set generate one - seriesElement.addClass( - [ - options.classNames.series, - seriesClassName || - `${options.classNames.series}-${alphaNumerate(seriesIndex)}` - ].join(' ') - ); - - normalizedData.series[seriesIndex].forEach((value, valueIndex) => { - let labelAxisValueIndex; - // We need to set labelAxisValueIndex based on some options combinations + each( + data.series, + (series, seriesIndex) => { + // Calculating bi-polar value of index for seriesOffset. For i = 0..4 biPol will be -1.5, -0.5, 0.5, 1.5 etc. + const biPol = seriesIndex - (data.series.length - 1) / 2; + // Half of the period width between vertical grid lines used to position bars + let periodHalfLength: number; + + // We need to set periodHalfLength based on some options combinations if (options.distributeSeries && !options.stackBars) { - // If distributed series are enabled but stacked bars aren't, we can use the seriesIndex for later projection - // on the step axis for label positioning - labelAxisValueIndex = seriesIndex; + // If distributed series are enabled but stacked bars aren't, we need to use the length of the normaizedData array + // which is the series count and divide by 2 + periodHalfLength = + labelAxis.axisLength / normalizedData.series.length / 2; } else if (options.distributeSeries && options.stackBars) { - // If distributed series and stacked bars are enabled, we will only get one bar and therefore always use - // 0 for projection on the label step axis - labelAxisValueIndex = 0; + // If distributed series and stacked bars are enabled we'll only get one bar so we should just divide the axis + // length by 2 + periodHalfLength = labelAxis.axisLength / 2; } else { - // On regular bar charts we just use the value index to project on the label step axis - labelAxisValueIndex = valueIndex; + // On regular bar charts we should just use the series length + periodHalfLength = + labelAxis.axisLength / + normalizedData.series[seriesIndex].length / + 2; } - let projected; - // We need to transform coordinates differently based on the chart layout - if (options.horizontalBars) { - projected = { - x: - chartRect.x1 + - valueAxis.projectValue( - safeHasProperty(value, 'x') ? value.x : 0, - valueIndex, - normalizedData.series[seriesIndex] - ), - y: - chartRect.y1 - - labelAxis.projectValue( - safeHasProperty(value, 'y') ? value.y : 0, - labelAxisValueIndex, - normalizedData.series[seriesIndex] - ) - }; - } else { - projected = { - x: - chartRect.x1 + - labelAxis.projectValue( - safeHasProperty(value, 'x') ? value.x : 0, - labelAxisValueIndex, - normalizedData.series[seriesIndex] - ), - y: - chartRect.y1 - - valueAxis.projectValue( - safeHasProperty(value, 'y') ? value.y : 0, - valueIndex, - normalizedData.series[seriesIndex] - ) - }; + // Adding the series group to the series element + const seriesElement = seriesGroup.elem('g'); + const seriesName = safeHasProperty(series, 'name') && series.name; + const seriesClassName = + safeHasProperty(series, 'className') && series.className; + const seriesMeta = safeHasProperty(series, 'meta') + ? series.meta + : undefined; + + // Write attributes to series group element. If series name or meta is undefined the attributes will not be written + if (seriesName) { + seriesElement.attr({ + 'ct:series-name': seriesName + }); } - // If the label axis is a step based axis we will offset the bar into the middle of between two steps using - // the periodHalfLength value. Also we do arrange the different series so that they align up to each other using - // the seriesBarDistance. If we don't have a step axis, the bar positions can be chosen freely so we should not - // add any automated positioning. - if (labelAxis instanceof StepAxis) { - // Offset to center bar between grid lines, but only if the step axis is not stretched - if (!labelAxis.stretch) { + if (seriesMeta) { + seriesElement.attr({ + 'ct:meta': serialize(seriesMeta) + }); + } + + // Use series class from series data or if not set generate one + seriesElement.addClass( + [ + options.classNames.series, + seriesClassName || + `${options.classNames.series}-${alphaNumerate(seriesIndex)}` + ].join(' ') + ); + + normalizedData.series[seriesIndex].forEach((value, valueIndex) => { + let labelAxisValueIndex; + // We need to set labelAxisValueIndex based on some options combinations + if (options.distributeSeries && !options.stackBars) { + // If distributed series are enabled but stacked bars aren't, we can use the seriesIndex for later projection + // on the step axis for label positioning + labelAxisValueIndex = seriesIndex; + } else if (options.distributeSeries && options.stackBars) { + // If distributed series and stacked bars are enabled, we will only get one bar and therefore always use + // 0 for projection on the label step axis + labelAxisValueIndex = 0; + } else { + // On regular bar charts we just use the value index to project on the label step axis + labelAxisValueIndex = valueIndex; + } + + let projected; + // We need to transform coordinates differently based on the chart layout + if (options.horizontalBars) { + projected = { + x: + chartRect.x1 + + valueAxis.projectValue( + safeHasProperty(value, 'x') ? value.x : 0, + valueIndex, + normalizedData.series[seriesIndex] + ), + y: + chartRect.y1 - + labelAxis.projectValue( + safeHasProperty(value, 'y') ? value.y : 0, + labelAxisValueIndex, + normalizedData.series[seriesIndex] + ) + }; + } else { + projected = { + x: + chartRect.x1 + + labelAxis.projectValue( + safeHasProperty(value, 'x') ? value.x : 0, + labelAxisValueIndex, + normalizedData.series[seriesIndex] + ), + y: + chartRect.y1 - + valueAxis.projectValue( + safeHasProperty(value, 'y') ? value.y : 0, + valueIndex, + normalizedData.series[seriesIndex] + ) + }; + } + + // If the label axis is a step based axis we will offset the bar into the middle of between two steps using + // the periodHalfLength value. Also we do arrange the different series so that they align up to each other using + // the seriesBarDistance. If we don't have a step axis, the bar positions can be chosen freely so we should not + // add any automated positioning. + if (labelAxis instanceof StepAxis) { + // Offset to center bar between grid lines, but only if the step axis is not stretched + if (!labelAxis.stretch) { + projected[labelAxis.units.pos] += + periodHalfLength * (options.horizontalBars ? -1 : 1); + } + // Using bi-polar offset for multiple series if no stacked bars or series distribution is used projected[labelAxis.units.pos] += - periodHalfLength * (options.horizontalBars ? -1 : 1); + options.stackBars || options.distributeSeries + ? 0 + : biPol * + options.seriesBarDistance * + (options.horizontalBars ? -1 : 1); } - // Using bi-polar offset for multiple series if no stacked bars or series distribution is used - projected[labelAxis.units.pos] += - options.stackBars || options.distributeSeries - ? 0 - : biPol * - options.seriesBarDistance * - (options.horizontalBars ? -1 : 1); - } - // Enter value in stacked bar values used to remember previous screen value for stacking up bars - const previousStack = stackedBarValues[valueIndex] || zeroPoint; - stackedBarValues[valueIndex] = - previousStack - (zeroPoint - projected[labelAxis.counterUnits.pos]); + // Enter value in stacked bar values used to remember previous screen value for stacking up bars + const previousStack = stackedBarValues[valueIndex] || zeroPoint; + stackedBarValues[valueIndex] = + previousStack - (zeroPoint - projected[labelAxis.counterUnits.pos]); - // Skip if value is undefined - if (value === undefined) { - return; - } + // Skip if value is undefined + if (value === undefined) { + return; + } - const positions = { - [`${labelAxis.units.pos}1`]: projected[labelAxis.units.pos], - [`${labelAxis.units.pos}2`]: projected[labelAxis.units.pos] - } as Record<'x1' | 'y1' | 'x2' | 'y2', number>; - - if ( - options.stackBars && - (options.stackMode === 'accumulate' || !options.stackMode) - ) { - // Stack mode: accumulate (default) - // If bars are stacked we use the stackedBarValues reference and otherwise base all bars off the zero line - // We want backwards compatibility, so the expected fallback without the 'stackMode' option - // to be the original behaviour (accumulate) - positions[`${labelAxis.counterUnits.pos}1`] = previousStack; - positions[`${labelAxis.counterUnits.pos}2`] = - stackedBarValues[valueIndex]; - } else { - // Draw from the zero line normally - // This is also the same code for Stack mode: overlap - positions[`${labelAxis.counterUnits.pos}1`] = zeroPoint; - positions[`${labelAxis.counterUnits.pos}2`] = - projected[labelAxis.counterUnits.pos]; - } + const positions = { + [`${labelAxis.units.pos}1`]: projected[labelAxis.units.pos], + [`${labelAxis.units.pos}2`]: projected[labelAxis.units.pos] + } as Record<'x1' | 'y1' | 'x2' | 'y2', number>; + + if ( + options.stackBars && + (options.stackMode === 'accumulate' || !options.stackMode) + ) { + // Stack mode: accumulate (default) + // If bars are stacked we use the stackedBarValues reference and otherwise base all bars off the zero line + // We want backwards compatibility, so the expected fallback without the 'stackMode' option + // to be the original behaviour (accumulate) + positions[`${labelAxis.counterUnits.pos}1`] = previousStack; + positions[`${labelAxis.counterUnits.pos}2`] = + stackedBarValues[valueIndex]; + } else { + // Draw from the zero line normally + // This is also the same code for Stack mode: overlap + positions[`${labelAxis.counterUnits.pos}1`] = zeroPoint; + positions[`${labelAxis.counterUnits.pos}2`] = + projected[labelAxis.counterUnits.pos]; + } - // Limit x and y so that they are within the chart rect - positions.x1 = Math.min( - Math.max(positions.x1, chartRect.x1), - chartRect.x2 - ); - positions.x2 = Math.min( - Math.max(positions.x2, chartRect.x1), - chartRect.x2 - ); - positions.y1 = Math.min( - Math.max(positions.y1, chartRect.y2), - chartRect.y1 - ); - positions.y2 = Math.min( - Math.max(positions.y2, chartRect.y2), - chartRect.y1 - ); + // Limit x and y so that they are within the chart rect + positions.x1 = Math.min( + Math.max(positions.x1, chartRect.x1), + chartRect.x2 + ); + positions.x2 = Math.min( + Math.max(positions.x2, chartRect.x1), + chartRect.x2 + ); + positions.y1 = Math.min( + Math.max(positions.y1, chartRect.y2), + chartRect.y1 + ); + positions.y2 = Math.min( + Math.max(positions.y2, chartRect.y2), + chartRect.y1 + ); - const metaData = getMetaData(series, valueIndex); - - // Create bar element - const bar = seriesElement - .elem('line', positions, options.classNames.bar) - .attr({ - 'ct:value': [ - safeHasProperty(value, 'x') && value.x, - safeHasProperty(value, 'y') && value.y - ] - .filter(isNumeric) - .join(','), - 'ct:meta': serialize(metaData) + const metaData = getMetaData(series, valueIndex); + + // Create bar element + const bar = seriesElement + .elem('line', positions, options.classNames.bar) + .attr({ + 'ct:value': [ + safeHasProperty(value, 'x') && value.x, + safeHasProperty(value, 'y') && value.y + ] + .filter(isNumeric) + .join(','), + 'ct:meta': serialize(metaData) + }); + + this.eventEmitter.emit('draw', { + type: 'bar', + value, + index: valueIndex, + meta: metaData, + series, + seriesIndex, + axisX, + axisY, + chartRect, + group: seriesElement, + element: bar, + ...positions }); - - this.eventEmitter.emit('draw', { - type: 'bar', - value, - index: valueIndex, - meta: metaData, - series, - seriesIndex, - axisX, - axisY, - chartRect, - group: seriesElement, - element: bar, - ...positions }); - }); - }); + }, + options.reverseData + ); this.eventEmitter.emit('created', { chartRect, diff --git a/src/charts/LineChart/LineChart.spec.ts b/src/charts/LineChart/LineChart.spec.ts index 4e272d53..d372440e 100644 --- a/src/charts/LineChart/LineChart.spec.ts +++ b/src/charts/LineChart/LineChart.spec.ts @@ -1028,5 +1028,33 @@ describe('Charts', () => { ).not.toBe('NaN'); }); }); + + it('should correct apply class names', async () => { + data = { + labels: ['A', 'B', 'C'], + series: [ + { + className: 'series-1', + data: [1, 2, 3] + }, + { + className: 'series-2', + data: [4, 5, 6] + } + ] + }; + options = { + reverseData: true + }; + await createChart(); + + const seriesElements = document.querySelectorAll('.ct-series'); + + expect(seriesElements[0]).toHaveClass('series-2'); + expect(seriesElements[0]).toContainHTML('ct:value="6"'); + + expect(seriesElements[1]).toHaveClass('series-1'); + expect(seriesElements[1]).toContainHTML('ct:value="3"'); + }); }); }); diff --git a/src/charts/LineChart/LineChart.ts b/src/charts/LineChart/LineChart.ts index cf6d9004..e0eb9691 100644 --- a/src/charts/LineChart/LineChart.ts +++ b/src/charts/LineChart/LineChart.ts @@ -24,7 +24,7 @@ import { createChartRect, createGridBackground } from '../../core'; -import { isNumeric, noop, extend, safeHasProperty } from '../../utils'; +import { isNumeric, noop, extend, safeHasProperty, each } from '../../utils'; import { StepAxis, AutoScaleAxis, axisUnits } from '../../axes'; import { monotoneCubic, none } from '../../interpolation'; import { BaseChart } from './../BaseChart'; @@ -330,233 +330,237 @@ export class LineChart extends BaseChart { } // Draw the series - data.series.forEach((series, seriesIndex) => { - const seriesElement = seriesGroup.elem('g'); - const seriesName = safeHasProperty(series, 'name') && series.name; - const seriesClassName = - safeHasProperty(series, 'className') && series.className; - const seriesMeta = safeHasProperty(series, 'meta') - ? series.meta - : undefined; - - // Write attributes to series group element. If series name or meta is undefined the attributes will not be written - if (seriesName) { - seriesElement.attr({ - 'ct:series-name': seriesName - }); - } + each( + data.series, + (series, seriesIndex) => { + const seriesElement = seriesGroup.elem('g'); + const seriesName = safeHasProperty(series, 'name') && series.name; + const seriesClassName = + safeHasProperty(series, 'className') && series.className; + const seriesMeta = safeHasProperty(series, 'meta') + ? series.meta + : undefined; + + // Write attributes to series group element. If series name or meta is undefined the attributes will not be written + if (seriesName) { + seriesElement.attr({ + 'ct:series-name': seriesName + }); + } + + if (seriesMeta) { + seriesElement.attr({ + 'ct:meta': serialize(seriesMeta) + }); + } + + // Use series class from series data or if not set generate one + seriesElement.addClass( + [ + options.classNames.series, + seriesClassName || + `${options.classNames.series}-${alphaNumerate(seriesIndex)}` + ].join(' ') + ); - if (seriesMeta) { - seriesElement.attr({ - 'ct:meta': serialize(seriesMeta) + const pathCoordinates: number[] = []; + const pathData: SegmentData[] = []; + + normalizedData.series[seriesIndex].forEach((value, valueIndex) => { + const p = { + x: + chartRect.x1 + + axisX.projectValue( + value, + valueIndex, + normalizedData.series[seriesIndex] + ), + y: + chartRect.y1 - + axisY.projectValue( + value, + valueIndex, + normalizedData.series[seriesIndex] + ) + }; + pathCoordinates.push(p.x, p.y); + pathData.push({ + value, + valueIndex, + meta: getMetaData(series, valueIndex) + }); }); - } - - // Use series class from series data or if not set generate one - seriesElement.addClass( - [ - options.classNames.series, - seriesClassName || - `${options.classNames.series}-${alphaNumerate(seriesIndex)}` - ].join(' ') - ); - const pathCoordinates: number[] = []; - const pathData: SegmentData[] = []; - - normalizedData.series[seriesIndex].forEach((value, valueIndex) => { - const p = { - x: - chartRect.x1 + - axisX.projectValue( - value, - valueIndex, - normalizedData.series[seriesIndex] - ), - y: - chartRect.y1 - - axisY.projectValue( - value, - valueIndex, - normalizedData.series[seriesIndex] - ) + const seriesOptions = { + lineSmooth: getSeriesOption(series, options, 'lineSmooth'), + showPoint: getSeriesOption(series, options, 'showPoint'), + showLine: getSeriesOption(series, options, 'showLine'), + showArea: getSeriesOption(series, options, 'showArea'), + areaBase: getSeriesOption(series, options, 'areaBase') }; - pathCoordinates.push(p.x, p.y); - pathData.push({ - value, - valueIndex, - meta: getMetaData(series, valueIndex) - }); - }); - const seriesOptions = { - lineSmooth: getSeriesOption(series, options, 'lineSmooth'), - showPoint: getSeriesOption(series, options, 'showPoint'), - showLine: getSeriesOption(series, options, 'showLine'), - showArea: getSeriesOption(series, options, 'showArea'), - areaBase: getSeriesOption(series, options, 'areaBase') - }; - - let smoothing; - if (typeof seriesOptions.lineSmooth === 'function') { - smoothing = seriesOptions.lineSmooth; - } else { - smoothing = seriesOptions.lineSmooth ? monotoneCubic() : none(); - } - - // Interpolating path where pathData will be used to annotate each path element so we can trace back the original - // index, value and meta data - const path = smoothing(pathCoordinates, pathData); - - // If we should show points we need to create them now to avoid secondary loop - // Points are drawn from the pathElements returned by the interpolation function - // Small offset for Firefox to render squares correctly - if (seriesOptions.showPoint) { - path.pathElements.forEach(pathElement => { - const { data: pathElementData } = pathElement; - const point = seriesElement.elem( - 'line', - { - x1: pathElement.x, - y1: pathElement.y, - x2: pathElement.x + 0.01, - y2: pathElement.y - }, - options.classNames.point - ); + let smoothing; + if (typeof seriesOptions.lineSmooth === 'function') { + smoothing = seriesOptions.lineSmooth; + } else { + smoothing = seriesOptions.lineSmooth ? monotoneCubic() : none(); + } + + // Interpolating path where pathData will be used to annotate each path element so we can trace back the original + // index, value and meta data + const path = smoothing(pathCoordinates, pathData); + + // If we should show points we need to create them now to avoid secondary loop + // Points are drawn from the pathElements returned by the interpolation function + // Small offset for Firefox to render squares correctly + if (seriesOptions.showPoint) { + path.pathElements.forEach(pathElement => { + const { data: pathElementData } = pathElement; + const point = seriesElement.elem( + 'line', + { + x1: pathElement.x, + y1: pathElement.y, + x2: pathElement.x + 0.01, + y2: pathElement.y + }, + options.classNames.point + ); - if (pathElementData) { - let x: number | undefined; - let y: number | undefined; + if (pathElementData) { + let x: number | undefined; + let y: number | undefined; - if (safeHasProperty(pathElementData.value, 'x')) { - x = pathElementData.value.x; - } + if (safeHasProperty(pathElementData.value, 'x')) { + x = pathElementData.value.x; + } + + if (safeHasProperty(pathElementData.value, 'y')) { + y = pathElementData.value.y; + } - if (safeHasProperty(pathElementData.value, 'y')) { - y = pathElementData.value.y; + point.attr({ + 'ct:value': [x, y].filter(isNumeric).join(','), + 'ct:meta': serialize(pathElementData.meta) + }); } - point.attr({ - 'ct:value': [x, y].filter(isNumeric).join(','), - 'ct:meta': serialize(pathElementData.meta) + this.eventEmitter.emit('draw', { + type: 'point', + value: pathElementData?.value, + index: pathElementData?.valueIndex || 0, + meta: pathElementData?.meta, + series, + seriesIndex, + axisX, + axisY, + group: seriesElement, + element: point, + x: pathElement.x, + y: pathElement.y, + chartRect }); - } + }); + } - this.eventEmitter.emit('draw', { - type: 'point', - value: pathElementData?.value, - index: pathElementData?.valueIndex || 0, - meta: pathElementData?.meta, + if (seriesOptions.showLine) { + const line = seriesElement.elem( + 'path', + { + d: path.stringify() + }, + options.classNames.line, + true + ); + + this.eventEmitter.emit('draw', { + type: 'line', + values: normalizedData.series[seriesIndex], + path: path.clone(), + chartRect, + // TODO: Remove redundant + index: seriesIndex, series, seriesIndex, + meta: seriesMeta, axisX, axisY, group: seriesElement, - element: point, - x: pathElement.x, - y: pathElement.y, - chartRect + element: line }); - }); - } - - if (seriesOptions.showLine) { - const line = seriesElement.elem( - 'path', - { - d: path.stringify() - }, - options.classNames.line, - true - ); - - this.eventEmitter.emit('draw', { - type: 'line', - values: normalizedData.series[seriesIndex], - path: path.clone(), - chartRect, - // TODO: Remove redundant - index: seriesIndex, - series, - seriesIndex, - meta: seriesMeta, - axisX, - axisY, - group: seriesElement, - element: line - }); - } - - // Area currently only works with axes that support a range! - if (seriesOptions.showArea && axisY.range) { - // If areaBase is outside the chart area (< min or > max) we need to set it respectively so that - // the area is not drawn outside the chart area. - const areaBase = Math.max( - Math.min(seriesOptions.areaBase, axisY.range.max), - axisY.range.min - ); - - // We project the areaBase value into screen coordinates - const areaBaseProjected = chartRect.y1 - axisY.projectValue(areaBase); - - // In order to form the area we'll first split the path by move commands so we can chunk it up into segments - path - .splitByCommand('M') - // We filter only "solid" segments that contain more than one point. Otherwise there's no need for an area - .filter(pathSegment => pathSegment.pathElements.length > 1) - .map(solidPathSegments => { - // Receiving the filtered solid path segments we can now convert those segments into fill areas - const firstElement = solidPathSegments.pathElements[0]; - const lastElement = - solidPathSegments.pathElements[ - solidPathSegments.pathElements.length - 1 - ]; - - // Cloning the solid path segment with closing option and removing the first move command from the clone - // We then insert a new move that should start at the area base and draw a straight line up or down - // at the end of the path we add an additional straight line to the projected area base value - // As the closing option is set our path will be automatically closed - return solidPathSegments - .clone(true) - .position(0) - .remove(1) - .move(firstElement.x, areaBaseProjected) - .line(firstElement.x, firstElement.y) - .position(solidPathSegments.pathElements.length + 1) - .line(lastElement.x, areaBaseProjected); - }) - .forEach(areaPath => { - // For each of our newly created area paths, we'll now create path elements by stringifying our path objects - // and adding the created DOM elements to the correct series group - const area = seriesElement.elem( - 'path', - { - d: areaPath.stringify() - }, - options.classNames.area, - true - ); + } + + // Area currently only works with axes that support a range! + if (seriesOptions.showArea && axisY.range) { + // If areaBase is outside the chart area (< min or > max) we need to set it respectively so that + // the area is not drawn outside the chart area. + const areaBase = Math.max( + Math.min(seriesOptions.areaBase, axisY.range.max), + axisY.range.min + ); - // Emit an event for each area that was drawn - this.eventEmitter.emit('draw', { - type: 'area', - values: normalizedData.series[seriesIndex], - path: areaPath.clone(), - series, - seriesIndex, - axisX, - axisY, - chartRect, - // TODO: Remove redundant - index: seriesIndex, - group: seriesElement, - element: area, - meta: seriesMeta + // We project the areaBase value into screen coordinates + const areaBaseProjected = chartRect.y1 - axisY.projectValue(areaBase); + + // In order to form the area we'll first split the path by move commands so we can chunk it up into segments + path + .splitByCommand('M') + // We filter only "solid" segments that contain more than one point. Otherwise there's no need for an area + .filter(pathSegment => pathSegment.pathElements.length > 1) + .map(solidPathSegments => { + // Receiving the filtered solid path segments we can now convert those segments into fill areas + const firstElement = solidPathSegments.pathElements[0]; + const lastElement = + solidPathSegments.pathElements[ + solidPathSegments.pathElements.length - 1 + ]; + + // Cloning the solid path segment with closing option and removing the first move command from the clone + // We then insert a new move that should start at the area base and draw a straight line up or down + // at the end of the path we add an additional straight line to the projected area base value + // As the closing option is set our path will be automatically closed + return solidPathSegments + .clone(true) + .position(0) + .remove(1) + .move(firstElement.x, areaBaseProjected) + .line(firstElement.x, firstElement.y) + .position(solidPathSegments.pathElements.length + 1) + .line(lastElement.x, areaBaseProjected); + }) + .forEach(areaPath => { + // For each of our newly created area paths, we'll now create path elements by stringifying our path objects + // and adding the created DOM elements to the correct series group + const area = seriesElement.elem( + 'path', + { + d: areaPath.stringify() + }, + options.classNames.area, + true + ); + + // Emit an event for each area that was drawn + this.eventEmitter.emit('draw', { + type: 'area', + values: normalizedData.series[seriesIndex], + path: areaPath.clone(), + series, + seriesIndex, + axisX, + axisY, + chartRect, + // TODO: Remove redundant + index: seriesIndex, + group: seriesElement, + element: area, + meta: seriesMeta + }); }); - }); - } - }); + } + }, + options.reverseData + ); this.eventEmitter.emit('created', { chartRect, diff --git a/src/charts/PieChart/PieChart.ts b/src/charts/PieChart/PieChart.ts index 1ced4647..084957b8 100644 --- a/src/charts/PieChart/PieChart.ts +++ b/src/charts/PieChart/PieChart.ts @@ -69,8 +69,6 @@ const defaultOptions = { labelInterpolationFnc: noop, // Label direction can be 'neutral', 'explode' or 'implode'. The labels anchor will be positioned based on those settings as well as the fact if the labels are on the right or left side of the center of the chart. Usually explode is useful when labels are positioned far away from the center. labelDirection: 'neutral', - // If true the whole data is reversed including labels, the series order as well as the whole series data arrays. - reverseData: false, // If true empty values will be ignored to avoid drawing unnecessary slices and labels ignoreEmptyValues: false }; diff --git a/src/charts/PieChart/PieChart.types.ts b/src/charts/PieChart/PieChart.types.ts index fd8fac88..cd7882ae 100644 --- a/src/charts/PieChart/PieChart.types.ts +++ b/src/charts/PieChart/PieChart.types.ts @@ -78,10 +78,6 @@ export interface PieChartOptions extends Omit { * Usually explode is useful when labels are positioned far away from the center. */ labelDirection?: LabelDirection; - /** - * If true the whole data is reversed including labels, the series order as well as the whole series data arrays. - */ - reverseData?: boolean; /** * If true empty values will be ignored to avoid drawing unnecessary slices and labels */ diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 0f65aa7a..2e8ad215 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -55,3 +55,19 @@ export function isArrayOfArrays(data: unknown): data is unknown[][] { return data.every(Array.isArray); } + +/** + * Loop over array. + */ +export function each( + list: T[], + callback: (item: T, index: number, itemIndex: number) => void, + reverse = false +) { + let index = 0; + + list[reverse ? 'reduceRight' : 'reduce']( + (_, item, itemIndex) => callback(item, index++, itemIndex), + void 0 + ); +}