diff --git a/docs/views/barChart/api/barChart.md b/docs/views/barChart/api/barChart.md index a8b6532e4..5b0029b50 100644 --- a/docs/views/barChart/api/barChart.md +++ b/docs/views/barChart/api/barChart.md @@ -100,8 +100,9 @@ const chartData = { | horizontal | Boolean | null | horizontal Bar 차트 표시를 위한 속성 | true / false | | interval | String | null | 축에 표시되는 값의 간격 단위 (축의 타입에 따라 달라짐) | labelStyle | Object | ([상세](#labelstyle)) | 라벨의 폰트 스타일을 설정 | | - | formatter | function | null | 데이터가 표시되기 전에 데이터의 형식을 지정하는 데 사용 | (value) => value + '%' | | plotLines | Array | ([상세](#plotline)) | plot line(임계선 표시 용도) 설정 | | + | plotBands | Array | ([상세](#plotband)) | plot band(임계영역 표시 용도) 설정 | | + | formatter | function | null | 데이터가 표시되기 전에 데이터의 형식을 지정하는 데 사용 | (value) => value + '%' | ##### linear type - interval (Axis Label 표기를 위한 interval) @@ -143,11 +144,20 @@ const chartData = { | value | Number(value), Date, Number(Index) | null | 선을 표시할 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | | color | Hex, RGB, RGBA Code(String) | '#FF0000' | 선 색상 | | | segments | Array | null | dash 간격 | [6, 2] | -| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlinelabel)) | +| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlabel)) | + +##### plotBand +| 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | +|-----|------|-------|-----|-----| +| from | Number(value), Date, Number(Index) | null | 박스를 표시할 시작 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | +| to | Number(value), Date, Number(Index) | null | 박스를 표시할 종료 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | +| color | Hex, RGB, RGBA Code(String) | '#FF0000' | 선 색상 | | +| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlabel)) | -##### plotLineLabel +##### plotLabel | 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | |-----|------|-------|-----|-----| +| show | Boolean | false | label 표시 여부 | true / false | | fontSize | Number | 12 | 폰트 크기 | | | fontColor | Hex, RGB, RGBA Code(String) | '#FF0000' | 폰트 색상 | | | fillColor | Hex, RGB, RGBA Code(String) | '#FFFFFF' | 박스 배경 색상 | | @@ -157,6 +167,8 @@ const chartData = { | fontFamily | String | 'Roboto' | 폰트 스타일 | | | textAlign | String | 'center' | 수평 정렬 | 'left', 'center', 'right' | | verticalAlign | String | 'middle' | 수직 정렬 | 'top', 'middle', 'bottom' | +| textOverflow | String | 'none' | 라벨을 넣을 수 있는 여백 혹은 maxWidth 값을 넘었을 경우의 처리방안 | 'none', 'ellipsis' | +| maxWidth | Number | null | 라벨의 최대 너비 | | #### title | 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | diff --git a/docs/views/barChart/example/Column.vue b/docs/views/barChart/example/Column.vue index 36cd4c9e5..622d4a389 100644 --- a/docs/views/barChart/example/Column.vue +++ b/docs/views/barChart/example/Column.vue @@ -54,23 +54,6 @@ startToZero: true, autoScaleRatio: 0.1, showGrid: false, - plotLines: [{ - value: 180, - color: '#FFA500', - label: { - text: 'Warning', - fontColor: '#FFA500', - }, - }, { - value: 300, - label: { - lineWidth: 1, - lineColor: '#000000', - fillColor: '#FF0000', - fontColor: '#FFFFFF', - text: 'Critical', - }, - }], }], }; diff --git a/docs/views/barChart/example/Horizontal.vue b/docs/views/barChart/example/Horizontal.vue index d12d899c1..42518542e 100644 --- a/docs/views/barChart/example/Horizontal.vue +++ b/docs/views/barChart/example/Horizontal.vue @@ -64,7 +64,6 @@ }, tooltip: { use: true, - formatter: addUnit, }, }; diff --git a/docs/views/barChart/example/PlotLine.vue b/docs/views/barChart/example/PlotLine.vue new file mode 100644 index 000000000..656892731 --- /dev/null +++ b/docs/views/barChart/example/PlotLine.vue @@ -0,0 +1,105 @@ + + + diff --git a/docs/views/barChart/props.js b/docs/views/barChart/props.js index fc688d59f..e467b8efc 100644 --- a/docs/views/barChart/props.js +++ b/docs/views/barChart/props.js @@ -14,6 +14,8 @@ import Event from './example/Event'; import EventRaw from '!!raw-loader!./example/Event'; import Gradient from './example/Gradient'; import GradientRaw from '!!raw-loader!./example/Gradient'; +import PlotLine from './example/PlotLine'; +import PlotLineRaw from '!!raw-loader!./example/PlotLine'; export default { mdText, @@ -53,5 +55,10 @@ export default { component: Gradient, parsedData: parseComponent(GradientRaw), }, + 'Plot line & Plot band': { + description: '차트 배경에 선 및 영역을 표시할 수 있습니다.', + component: PlotLine, + parsedData: parseComponent(PlotLineRaw), + }, }, }; diff --git a/docs/views/lineChart/api/lineChart.md b/docs/views/lineChart/api/lineChart.md index e67517941..3ebd09c2c 100644 --- a/docs/views/lineChart/api/lineChart.md +++ b/docs/views/lineChart/api/lineChart.md @@ -85,7 +85,7 @@ const chartData = | interval | String | null | 축에 표시되는 값의 간격 단위 (ex. 'day', 'hour', 'minute'...) | labelStyle | Object | ([상세](#labelstyle)) | 라벨의 폰트 스타일을 설정 | | | plotLines | Array | ([상세](#plotline)) | plot line(임계선 표시 용도) 설정 | | - + | plotBands | Array | ([상세](#plotband)) | plot band(임계영역 표시 용도) 설정 | | | formatter | function | null | 데이터가 표시되기 전에 데이터의 형식을 지정하는 데 사용 | (value) => value + '%' | ##### time type @@ -111,11 +111,20 @@ const chartData = | value | Number(value), Date, Number(Index) | null | 선을 표시할 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | | color | Hex, RGB, RGBA Code(String) | '#FF0000' | 선 색상 | | | segments | Array | null | dash 간격 | [6, 2] | -| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlinelabel)) | +| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlabel)) | -##### plotLineLabel +##### plotBand | 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | |-----|------|-------|-----|-----| +| from | Number(value), Date, Number(Index) | null | 박스를 표시할 시작 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | +| to | Number(value), Date, Number(Index) | null | 박스를 표시할 종료 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | +| color | Hex, RGB, RGBA Code(String) | '#FF0000' | 선 색상 | | +| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlabel)) | + +##### plotLabel +| 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | +|-----|------|-------|-----|-----| +| show | Boolean | false | label 표시 여부 | true / false | | fontSize | Number | 12 | 폰트 크기 | | | fontColor | Hex, RGB, RGBA Code(String) | '#FF0000' | 폰트 색상 | | | fillColor | Hex, RGB, RGBA Code(String) | '#FFFFFF' | 박스 배경 색상 | | @@ -125,6 +134,8 @@ const chartData = | fontFamily | String | 'Roboto' | 폰트 스타일 | | | textAlign | String | 'center' | 수평 정렬 | 'left', 'center', 'right' | | verticalAlign | String | 'middle' | 수직 정렬 | 'top', 'middle', 'bottom' | +| textOverflow | String | 'none' | 라벨을 넣을 수 있는 여백 혹은 maxWidth 값을 넘었을 경우의 처리방안 | 'none', 'ellipsis' | +| maxWidth | Number | null | 라벨의 최대 너비 | | #### title | 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | diff --git a/docs/views/lineChart/example/Default.vue b/docs/views/lineChart/example/Default.vue index 4405eb4dc..6fe0876d7 100644 --- a/docs/views/lineChart/example/Default.vue +++ b/docs/views/lineChart/example/Default.vue @@ -19,14 +19,21 @@ export default { setup() { - const crushTime = ref(); const chartData = reactive({ series: { series1: { name: 'series#1' }, + series2: { name: 'series#2' }, + series3: { name: 'series#3' }, + series4: { name: 'series#4' }, + series5: { name: 'series#5' }, }, labels: [], data: { series1: [], + series2: [], + series3: [], + series4: [], + series5: [], }, }); @@ -49,28 +56,12 @@ type: 'time', timeFormat: 'HH:mm:ss', interval: 'second', - plotLines: [{ - color: '#FF0000', - value: crushTime, - segments: [6, 2], - label: { - text: 'Crush', - }, - }], }], axesY: [{ type: 'linear', showGrid: true, startToZero: true, autoScaleRatio: 0.1, - plotLines: [{ - color: '#FFA500', - value: 3000, - label: { - text: 'Caution', - fontColor: '#FFA500', - }, - }], }], }); @@ -91,12 +82,7 @@ seriesData.shift(); } - const randomValue = Math.floor(Math.random() * ((5000 - 5) + 1)) + 5; - seriesData.push(randomValue); - - if (randomValue > 4800) { - crushTime.value = timeValue; - } + seriesData.push(Math.floor(Math.random() * ((5000 - 5) + 1)) + 5); }); }; diff --git a/docs/views/lineChart/example/PlotLine.vue b/docs/views/lineChart/example/PlotLine.vue new file mode 100644 index 000000000..ca9a90a2e --- /dev/null +++ b/docs/views/lineChart/example/PlotLine.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/docs/views/lineChart/props.js b/docs/views/lineChart/props.js index 51c332eb4..ae25e3b76 100644 --- a/docs/views/lineChart/props.js +++ b/docs/views/lineChart/props.js @@ -12,6 +12,8 @@ import DragSelection from './example/DragSelection'; import DragSelectionRaw from '!!raw-loader!./example/DragSelection'; import Tooltip from './example/Tooltip'; import TooltipRaw from '!!raw-loader!./example/Tooltip'; +import PlotLine from './example/PlotLine'; +import PlotLineRaw from '!!raw-loader!./example/PlotLine'; export default { mdText, @@ -46,5 +48,10 @@ export default { component: Tooltip, parsedData: parseComponent(TooltipRaw), }, + 'Plot line & Plot band': { + description: '차트 배경에 선 및 영역을 표시할 수 있습니다.', + component: PlotLine, + parsedData: parseComponent(PlotLineRaw), + }, }, }; diff --git a/docs/views/scatterChart/api/scatterChart.md b/docs/views/scatterChart/api/scatterChart.md index bedfb90e7..09319420f 100644 --- a/docs/views/scatterChart/api/scatterChart.md +++ b/docs/views/scatterChart/api/scatterChart.md @@ -78,6 +78,7 @@ const chartData = | interval | String | null | 축에 표시되는 값의 간격 단위 (ex. 'day', 'hour', 'minute'...) | labelStyle | Object | ([상세](#labelstyle)) | 라벨의 폰트 스타일을 설정 | | | plotLines | Array | ([상세](#plotline)) | plot line(임계선 표시 용도) 설정 | | + | plotBands | Array | ([상세](#plotband)) | plot band(임계영역 표시 용도) 설정 | | | formatter | function | null | 데이터가 표시되기 전에 데이터의 형식을 지정하는 데 사용 | (value) => value + '%' | ##### time type @@ -103,11 +104,20 @@ const chartData = | value | Number(value), Date, Number(Index) | null | 선을 표시할 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | | color | Hex, RGB, RGBA Code(String) | '#FF0000' | 선 색상 | | | segments | Array | null | dash 간격 | [6, 2] | -| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlinelabel)) | +| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlabel)) | -##### plotLineLabel +##### plotBand | 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | |-----|------|-------|-----|-----| +| from | Number(value), Date, Number(Index) | null | 박스를 표시할 시작 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | +| to | Number(value), Date, Number(Index) | null | 박스를 표시할 종료 위치에 해당하는 값 | 3000,
new Date(),
1 (축의 타입이 'step'인 경우 1번째 요소) | +| color | Hex, RGB, RGBA Code(String) | '#FF0000' | 선 색상 | | +| label | Object | null | 표시할 label의 스타일을 정의 | ([상세](#plotlabel)) | + +##### plotLabel +| 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | +|-----|------|-------|-----|-----| +| show | Boolean | false | label 표시 여부 | true / false | | fontSize | Number | 12 | 폰트 크기 | | | fontColor | Hex, RGB, RGBA Code(String) | '#FF0000' | 폰트 색상 | | | fillColor | Hex, RGB, RGBA Code(String) | '#FFFFFF' | 박스 배경 색상 | | @@ -117,6 +127,8 @@ const chartData = | fontFamily | String | 'Roboto' | 폰트 스타일 | | | textAlign | String | 'center' | 수평 정렬 | 'left', 'center', 'right' | | verticalAlign | String | 'middle' | 수직 정렬 | 'top', 'middle', 'bottom' | +| textOverflow | String | 'none' | 라벨을 넣을 수 있는 여백 혹은 maxWidth 값을 넘었을 경우의 처리방안 | 'none', 'ellipsis' | +| maxWidth | Number | null | 라벨의 최대 너비 | | #### title | 이름 | 타입 | 디폴트 | 설명 | 종류(예시) | diff --git a/docs/views/scatterChart/example/PlotLine.vue b/docs/views/scatterChart/example/PlotLine.vue new file mode 100644 index 000000000..12ae67d8f --- /dev/null +++ b/docs/views/scatterChart/example/PlotLine.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/docs/views/scatterChart/props.js b/docs/views/scatterChart/props.js index 3e07a28bc..719814e84 100644 --- a/docs/views/scatterChart/props.js +++ b/docs/views/scatterChart/props.js @@ -4,6 +4,8 @@ import Default from './example/Default'; import DefaultRaw from '!!raw-loader!./example/Default'; import Event from './example/Event'; import EventRaw from '!!raw-loader!./example/Event'; +import PlotLine from './example/PlotLine'; +import PlotLineRaw from '!!raw-loader!./example/PlotLine'; export default { mdText, @@ -18,5 +20,10 @@ export default { component: Event, parsedData: parseComponent(EventRaw), }, + 'Plot line & Plot band': { + description: '차트 배경에 선 및 영역을 표시할 수 있습니다.', + component: PlotLine, + parsedData: parseComponent(PlotLineRaw), + }, }, }; diff --git a/src/components/chart/helpers/helpers.constant.js b/src/components/chart/helpers/helpers.constant.js index 868bb3692..aa1c21243 100644 --- a/src/components/chart/helpers/helpers.constant.js +++ b/src/components/chart/helpers/helpers.constant.js @@ -102,6 +102,7 @@ export const PLOT_LINE_OPTION = { }; export const PLOT_LINE_LABEL_OPTION = { + show: false, fontSize: 12, fontColor: '#FF0000', fillColor: '#FFFFFF', @@ -111,8 +112,15 @@ export const PLOT_LINE_LABEL_OPTION = { fontFamily: 'Roboto', verticalAlign: 'middle', textAlign: 'center', + textOverflow: 'none', // 'none', 'ellipsis' + maxWidth: null, }; +export const PLOT_BAND_OPTION = { + color: '#FAE59D', +}; + + export const TIME_INTERVALS = { millisecond: { common: true, diff --git a/src/components/chart/scale/scale.js b/src/components/chart/scale/scale.js index d8b0f9514..d7023a5cc 100644 --- a/src/components/chart/scale/scale.js +++ b/src/components/chart/scale/scale.js @@ -5,6 +5,7 @@ import { AXIS_UNITS, PLOT_LINE_OPTION, PLOT_LINE_LABEL_OPTION, + PLOT_BAND_OPTION, } from '../helpers/helpers.constant'; import Util from '../helpers/helpers.util'; @@ -284,17 +285,48 @@ class Scale { ctx.closePath(); } - // Draw plot line - if (this.plotLines?.length) { + // Draw plot lines and plot bands + if (this.plotBands?.length || this.plotLines?.length) { const xArea = chartRect.chartWidth - (labelOffset.left + labelOffset.right); const yArea = chartRect.chartHeight - (labelOffset.top + labelOffset.bottom); const padding = aliasPixel + 1; const minX = aPos.x1 + padding; const maxX = aPos.x2; - const minY = aPos.y1 + padding; - const maxY = aPos.y2; + const minY = aPos.y1 + padding; // top + const maxY = aPos.y2; // bottom - this.plotLines.forEach((plotLine) => { + this.plotBands?.forEach((plotBand) => { + if (!plotBand.from && !plotBand.to) { + return; + } + + const mergedPlotBandOpt = defaultsDeep({}, plotBand, PLOT_BAND_OPTION); + const { from, to, label: labelOpt } = mergedPlotBandOpt; + + this.setPlotBandStyle(mergedPlotBandOpt); + + let fromPos; + let toPos; + if (this.type === 'x') { + fromPos = Canvas.calculateX(from ?? minX, axisMin, axisMax, xArea, minX); + toPos = Canvas.calculateX(to ?? maxX, axisMin, axisMax, xArea, minX); + this.drawXPlotBand(fromPos, toPos, minX, maxX, minY, maxY); + } else { + fromPos = Canvas.calculateY(from ?? axisMin, axisMin, axisMax, yArea, maxY); + toPos = Canvas.calculateY(to ?? axisMax, axisMin, axisMax, yArea, maxY); + this.drawYPlotBand(fromPos, toPos, minX, maxX, minY, maxY); + } + + if (labelOpt.show) { + const labelOptions = this.getNormalizedLabelOptions(chartRect, labelOpt); + const textXY = this.getPlotBandLabelPosition(fromPos, toPos, labelOptions, maxX, minY); + this.drawPlotLabel(labelOptions, textXY); + } + + ctx.restore(); + }); + + this.plotLines?.forEach((plotLine) => { if (!plotLine.value) { return; } @@ -304,12 +336,19 @@ class Scale { this.setPlotLineStyle(mergedPlotLineOpt); + let dataPos; if (this.type === 'x') { - const dataX = Canvas.calculateX(value, axisMin, axisMax, xArea, minX); - this.drawXPlotLine(dataX, minX, maxX, minY, maxY, labelOpt); + dataPos = Canvas.calculateX(value, axisMin, axisMax, xArea, minX); + this.drawXPlotLine(dataPos, minX, maxX, minY, maxY); } else { - const dataY = Canvas.calculateY(value, axisMin, axisMax, yArea, maxY); - this.drawYPlotLine(dataY, minX, maxX, minY, maxY, labelOpt); + dataPos = Canvas.calculateY(value, axisMin, axisMax, yArea, maxY); + this.drawYPlotLine(dataPos, minX, maxX, minY, maxY); + } + + if (labelOpt.show) { + const labelOptions = this.getNormalizedLabelOptions(chartRect, labelOpt); + const textXY = this.getPlotLineLabelPosition(dataPos, labelOptions, maxX, minY); + this.drawPlotLabel(labelOptions, textXY); } ctx.restore(); @@ -337,6 +376,55 @@ class Scale { } } + /** + * Set plot band style + * @param {object} plotBand plotBand Options + * + * @returns {undefined} + */ + setPlotBandStyle(plotBand) { + const ctx = this.ctx; + const { color } = plotBand; + + ctx.beginPath(); + ctx.save(); + ctx.fillStyle = color; + } + + /** + * Draw X Plot band + * @param {number} fromDataX From data's X Position + * @param {number} toDataX To data's X Position + * @param {number} minX Min X Position + * @param {number} maxX Max X Position + * @param {number} minY Min Y Position + * @param {number} maxY Max Y Position + * + * @returns {undefined} + */ + drawXPlotBand(fromDataX, toDataX, minX, maxX, minY, maxY) { + const ctx = this.ctx; + + const checkValidPosition = x => x || x > minX || x < maxX; + + if (!checkValidPosition(fromDataX) || !checkValidPosition(toDataX)) { + ctx.closePath(); + ctx.restore(); + return; + } + + ctx.moveTo(fromDataX, minY); + ctx.lineTo(fromDataX, maxY); + ctx.lineTo(toDataX, maxY); + ctx.lineTo(toDataX, minY); + ctx.lineTo(fromDataX, minY); + + ctx.stroke(); + ctx.fill(); + ctx.restore(); + ctx.closePath(); + } + /** * Draw X Plot line * @param {object} dataX Data's X Position @@ -344,11 +432,10 @@ class Scale { * @param {number} maxX Max X Position * @param {number} minY Min Y Position * @param {number} maxY Max Y Position - * @param {object} labelOpt plotLine Options * * @returns {undefined} */ - drawXPlotLine(dataX, minX, maxX, minY, maxY, labelOpt) { + drawXPlotLine(dataX, minX, maxX, minY, maxY) { const ctx = this.ctx; if (!dataX || dataX < minX || dataX > maxX) { @@ -363,51 +450,6 @@ class Scale { ctx.stroke(); ctx.restore(); ctx.closePath(); - - if (labelOpt) { - const mergedLabelOpt = defaultsDeep({}, labelOpt, PLOT_LINE_LABEL_OPTION); - - ctx.save(); - ctx.beginPath(); - ctx.font = Util.getLabelStyle(mergedLabelOpt); - - const { - fontSize, - labelBoxPadding, - labelHalfWidth, - } = this.getLabelParameters(mergedLabelOpt); - - if (fontSize <= 0) { - return; - } - - let textX; - switch (mergedLabelOpt.textAlign) { - case 'left': - textX = dataX - labelHalfWidth - labelBoxPadding; - break; - - case 'right': - textX = dataX + labelHalfWidth + labelBoxPadding; - break; - - case 'center': - default: - textX = dataX; - break; - } - - const textY = minY - labelBoxPadding - fontSize; - - this.drawPlotLineLabel(mergedLabelOpt, { - top: minY - (labelBoxPadding * 2) - fontSize, - bottom: minY - labelBoxPadding, - left: textX - labelHalfWidth - labelBoxPadding, - right: textX + labelHalfWidth + labelBoxPadding, - x: textX, - y: textY, - }); - } } /** @@ -417,11 +459,10 @@ class Scale { * @param {number} maxX Max X Position * @param {number} minY Min Y Position * @param {number} maxY Max Y Position - * @param {object} labelOpt plotLine Options * * @returns {undefined} */ - drawYPlotLine(dataY, minX, maxX, minY, maxY, labelOpt) { + drawYPlotLine(dataY, minX, maxX, minY, maxY) { const ctx = this.ctx; if (!dataY || dataY > maxY || dataY < minY) { @@ -436,91 +477,263 @@ class Scale { ctx.stroke(); ctx.restore(); ctx.closePath(); + } - if (labelOpt) { - const mergedLabelOpt = defaultsDeep({}, labelOpt, PLOT_LINE_LABEL_OPTION); + /** + * Draw Y Plot band + * @param {number} fromDataY From data's Y Position (bottom) + * @param {number} toDataY To data's Y Position (top) + * @param {number} minX Min X Position + * @param {number} maxX Max X Position + * @param {number} minY Min Y Position + * @param {number} maxY Max Y Position + * + * @returns {undefined} + */ + drawYPlotBand(fromDataY, toDataY, minX, maxX, minY, maxY) { + const ctx = this.ctx; - ctx.save(); - ctx.beginPath(); - ctx.font = Util.getLabelStyle(mergedLabelOpt); + const checkValidPosition = y => y || y > minY || y < maxY; + + if (!checkValidPosition(fromDataY) || !checkValidPosition(toDataY)) { + ctx.closePath(); + ctx.restore(); + return; + } + + ctx.moveTo(minX, fromDataY); + ctx.lineTo(minX, toDataY); + ctx.lineTo(maxX, toDataY); + ctx.lineTo(maxX, fromDataY); + ctx.lineTo(minX, fromDataY); + + ctx.fill(); + ctx.restore(); + ctx.closePath(); + } + + /** + * get normalized options for plot label + * @param {object} chartRect chartRect + * @param {object} labelOpt plotLine Options + * + * @returns {object} + */ + getNormalizedLabelOptions(chartRect, labelOpt) { + const mergedLabelOpt = defaultsDeep({}, labelOpt, PLOT_LINE_LABEL_OPTION); + + const ctx = this.ctx; + const { maxWidth } = mergedLabelOpt; + const fontSize = mergedLabelOpt.fontSize > 20 ? 20 : mergedLabelOpt.fontSize; + let label = mergedLabelOpt.text; + let labelWidth = maxWidth ?? ctx.measureText(label).width; + + const plotLabelAreaWidth = this.type === 'y' + ? chartRect.width - chartRect.chartWidth + : maxWidth ?? chartRect.width; + + if (plotLabelAreaWidth < ctx.measureText(label).width && mergedLabelOpt.textOverflow === 'ellipsis') { + label = Util.truncateLabelWithEllipsis(mergedLabelOpt.text, plotLabelAreaWidth, ctx); + labelWidth = ctx.measureText(label).width; + } + + return { + label, + fontSize, + labelWidth, + labelBoxPadding: fontSize / 4, + labelHalfWidth: labelWidth / 2, + labelHalfHeight: fontSize / 2, + ...mergedLabelOpt, + }; + } + + /** + * Calculate position of plot band's label + * @param {object} fromPos from data position + * @param {object} toPos to data position + * @param {object} labelOpt label options + * @param {object} maxX max x position + * @param {object} minY min y position + * + * @returns {object} + */ + getPlotBandLabelPosition(fromPos, toPos, labelOpt, maxX, minY) { + const { + fontSize, + labelWidth, + labelHalfWidth, + labelHalfHeight, + labelBoxPadding, + textAlign, + verticalAlign, + } = labelOpt; + + if (fontSize <= 0) { + return { textX: 0, textY: 0 }; + } + + let textX; + let textY; + + if (this.type === 'x') { + textY = minY - labelBoxPadding - fontSize; - const { - fontSize, - labelWidth, - labelHalfHeight, - labelBoxPadding, - } = this.getLabelParameters(mergedLabelOpt); + switch (textAlign) { + case 'left': + textX = fromPos + labelHalfWidth + labelBoxPadding; + break; - if (fontSize <= 0) { - return; + case 'right': + textX = toPos - labelHalfWidth - labelBoxPadding; + break; + + case 'center': + default: + textX = ((toPos - fromPos) / 2) + fromPos; + break; } + } else { + textX = maxX + labelWidth + labelBoxPadding; - let textY; - switch (mergedLabelOpt.verticalAlign) { + switch (verticalAlign) { case 'top': - textY = dataY - labelHalfHeight - labelBoxPadding; + textY = toPos + labelHalfHeight + labelBoxPadding; break; case 'bottom': - textY = dataY + labelHalfHeight + labelBoxPadding; + textY = fromPos - labelHalfHeight - labelBoxPadding; break; case 'middle': default: - textY = dataY; + textY = ((fromPos - toPos) / 2) + toPos; break; } - - const textX = maxX + labelWidth + labelBoxPadding; - - this.drawPlotLineLabel(mergedLabelOpt, { - top: textY - labelHalfHeight - labelBoxPadding, - bottom: textY + labelHalfHeight + labelBoxPadding, - left: textX - labelWidth - (labelBoxPadding / 2), - right: textX + labelBoxPadding, - x: textX, - y: textY, - }); } + + return { textX, textY }; } /** - * Calculate Values for drawing label - * @param {object} labelOpt plotLine Options + * Calculate position of plot line's label + * @param {object} dataPos data position + * @param {object} labelOpt label options + * @param {object} maxX max x position + * @param {object} minY min y position * - * @returns {object} + * @returns {undefined} */ - getLabelParameters(labelOpt) { - const ctx = this.ctx; - const fontSize = labelOpt.fontSize > 20 ? 20 : labelOpt.fontSize; - const labelBoxPadding = fontSize / 4; - const labelWidth = ctx.measureText(labelOpt.text).width; - const labelHalfWidth = labelWidth / 2; - const labelHalfHeight = fontSize / 2; - - return { + getPlotLineLabelPosition(dataPos, labelOpt, maxX, minY) { + const { fontSize, - labelBoxPadding, labelWidth, labelHalfWidth, labelHalfHeight, - }; + labelBoxPadding, + } = labelOpt; + + if (fontSize <= 0) { + return { textX: 0, textY: 0 }; + } + + let textX; + let textY; + + if (this.type === 'x') { + textY = minY - labelBoxPadding - fontSize; + + switch (labelOpt.textAlign) { + case 'left': + textX = dataPos - labelHalfWidth - labelBoxPadding; + break; + + case 'right': + textX = dataPos + labelHalfWidth + labelBoxPadding; + break; + + case 'center': + default: + textX = dataPos; + break; + } + } else { + textX = maxX + labelWidth + labelBoxPadding; + + switch (labelOpt.verticalAlign) { + case 'top': + textY = dataPos - labelHalfHeight - labelBoxPadding; + break; + + case 'bottom': + textY = dataPos + labelHalfHeight + labelBoxPadding; + break; + + case 'middle': + default: + textY = dataPos; + break; + } + } + + return { textX, textY }; } /** * Calculate Values for drawing label - * @param {object} labelOpt plot line Label Options - * @param {object} positions label positions + * @param {object} labelOptions plot line Label Options + * @param {object} positions x, y Position * * @returns {undefined} */ - drawPlotLineLabel(labelOpt, positions) { + drawPlotLabel(labelOptions, positions) { + if (!positions) { + return; + } + + const { textX, textY } = positions; + const { + label, + fontSize, + fontColor, + fillColor, + lineColor, + lineWidth, + labelBoxPadding, + labelWidth, + labelHalfWidth, + labelHalfHeight, + } = labelOptions; + + if (fontSize <= 0) { + return; + } + const ctx = this.ctx; - const { top, bottom, left, right, x, y } = positions; + ctx.save(); + ctx.beginPath(); + ctx.font = Util.getLabelStyle(labelOptions); + + let top = 0; + let bottom = 0; + let left = 0; + let right = 0; - ctx.fillStyle = labelOpt.fillColor; - ctx.strokeStyle = labelOpt.lineColor; - ctx.lineWidth = labelOpt.lineWidth; + if (this.type === 'x') { + top = textY - labelBoxPadding; + bottom = textY + fontSize; + left = textX - labelHalfWidth - labelBoxPadding; + right = textX + labelHalfWidth + labelBoxPadding; + } else { + top = textY - labelHalfHeight - labelBoxPadding; + bottom = textY + labelHalfHeight + labelBoxPadding; + left = textX - labelWidth; + right = textX + labelBoxPadding; + } + + ctx.fillStyle = fillColor; + ctx.strokeStyle = lineColor; + ctx.lineWidth = lineWidth; ctx.moveTo(left, bottom); ctx.lineTo(left, top); ctx.lineTo(right, top); @@ -528,12 +741,13 @@ class Scale { ctx.lineTo(left, bottom); ctx.fill(); - if (labelOpt.lineWidth > 0) { + if (lineWidth > 0) { ctx.stroke(); } - ctx.fillStyle = labelOpt.fontColor; - ctx.fillText(labelOpt.text, x, y); + ctx.fillStyle = fontColor; + + ctx.fillText(label, textX, textY); ctx.closePath(); } } diff --git a/src/components/chart/scale/scale.step.js b/src/components/chart/scale/scale.step.js index 3dfdb0f8c..93bfa0f81 100644 --- a/src/components/chart/scale/scale.step.js +++ b/src/components/chart/scale/scale.step.js @@ -1,5 +1,5 @@ import { defaultsDeep } from 'lodash-es'; -import { PLOT_LINE_OPTION } from '@/components/chart/helpers/helpers.constant'; +import { PLOT_BAND_OPTION, PLOT_LINE_OPTION } from '@/components/chart/helpers/helpers.constant'; import Scale from './scale'; import Util from '../helpers/helpers.util'; @@ -160,15 +160,42 @@ class StepScale extends Scale { ctx.closePath(); - // draw plot line - if (this.plotLines?.length) { + // draw plot lines and plot bands + if (this.plotBands?.length || this.plotLines?.length) { const padding = aliasPixel + 1; const minX = aPos.x1 + padding; const maxX = aPos.x2; const minY = aPos.y1 + padding; const maxY = aPos.y2; - this.plotLines.forEach((plotLine) => { + this.plotBands?.forEach((plotBand) => { + if (!plotBand.from && !plotBand.to) { + return; + } + + const mergedPlotBandOpt = defaultsDeep({}, plotBand, PLOT_BAND_OPTION); + const { from = 0, to = labels.length, label: labelOpt } = mergedPlotBandOpt; + const fromPos = Math.round(startPoint + (labelGap * from)); + const toPos = Math.round(startPoint + (labelGap * to)); + + this.setPlotBandStyle(mergedPlotBandOpt); + + if (this.type === 'x') { + this.drawXPlotBand(fromPos, toPos, minX, maxX, minY, maxY); + } else { + this.drawYPlotBand(fromPos, toPos, minX, maxX, minY, maxY); + } + + if (labelOpt.show) { + const labelOptions = this.getNormalizedLabelOptions(chartRect, labelOpt); + const textXY = this.getPlotBandLabelPosition(fromPos, toPos, labelOptions, maxX, minY); + this.drawPlotLabel(labelOptions, textXY); + } + + ctx.restore(); + }); + + this.plotLines?.forEach((plotLine) => { if (!plotLine.value) { return; } @@ -180,9 +207,15 @@ class StepScale extends Scale { this.setPlotLineStyle(mergedPlotLineOpt); if (this.type === 'x') { - this.drawXPlotLine(dataPos, minX, maxX, minY, maxY, labelOpt); + this.drawXPlotLine(dataPos, minX, maxX, minY, maxY); } else { - this.drawYPlotLine(dataPos, minX, maxX, minY, maxY, labelOpt); + this.drawYPlotLine(dataPos, minX, maxX, minY, maxY); + } + + if (labelOpt.show) { + const labelOptions = this.getNormalizedLabelOptions(chartRect, labelOpt); + const textXY = this.getPlotLineLabelPosition(dataPos, labelOptions, maxX, minY); + this.drawPlotLabel(labelOptions, textXY); } ctx.restore();