From 8aac07c99192d910adc6bdd490f0dc8db420bf69 Mon Sep 17 00:00:00 2001 From: Martin Lassen <37809937+martin-ebay@users.noreply.github.com> Date: Tue, 10 Jan 2023 10:15:58 -0800 Subject: [PATCH] highcharts charts (#1833) --- LICENSE.txt | 8 + README.md | 8 + package-lock.json | 11 + package.json | 1 + src/common/charts/bar-chart.js | 131 +++ src/common/charts/legend.js | 19 + src/common/charts/shared.js | 79 ++ src/common/event-utils/index.js | 11 + src/components/ebay-area-chart/README.md | 10 + .../ebay-area-chart/area-chart.stories.js | 137 +++ src/components/ebay-area-chart/component.js | 347 ++++++++ .../ebay-area-chart/examples/data.json | 777 ++++++++++++++++++ src/components/ebay-area-chart/index.marko | 22 + src/components/ebay-area-chart/marko-tag.json | 22 + src/components/ebay-area-chart/style.less | 23 + src/components/ebay-bar-chart/README.md | 10 + .../ebay-bar-chart/bar-chart.stories.js | 229 ++++++ src/components/ebay-bar-chart/component.js | 359 ++++++++ .../ebay-bar-chart/examples/data.json | 632 ++++++++++++++ src/components/ebay-bar-chart/index.marko | 20 + src/components/ebay-bar-chart/marko-tag.json | 23 + src/components/ebay-bar-chart/style.less | 15 + .../ebay-bar-chart/subtemplate.marko | 16 + src/components/ebay-line-chart/README.md | 10 + src/components/ebay-line-chart/component.js | 424 ++++++++++ .../ebay-line-chart/examples/data.json | 757 +++++++++++++++++ src/components/ebay-line-chart/index.marko | 21 + .../ebay-line-chart/line-chart.stories.js | 291 +++++++ src/components/ebay-line-chart/marko-tag.json | 23 + src/components/ebay-line-chart/style.less | 31 + src/components/ebay-line-chart/tooltip.marko | 13 + src/components/ebay-spark-line/README.md | 10 + src/components/ebay-spark-line/component.js | 56 ++ .../ebay-spark-line/examples/data.json | 136 +++ src/components/ebay-spark-line/index.marko | 16 + .../ebay-spark-line/spark-line.stories.js | 67 ++ src/components/ebay-spark-line/style.js | 1 + src/components/ebay-spark-line/style.less | 25 + .../renders-a-blue-line.expected.html | 17 + .../renders-a-green-line.expected.html | 17 + .../renders-a-red-line.expected.html | 17 + .../ebay-spark-line/test/test.server.js | 22 + 42 files changed, 4864 insertions(+) create mode 100644 src/common/charts/bar-chart.js create mode 100644 src/common/charts/legend.js create mode 100644 src/common/charts/shared.js create mode 100644 src/components/ebay-area-chart/README.md create mode 100644 src/components/ebay-area-chart/area-chart.stories.js create mode 100644 src/components/ebay-area-chart/component.js create mode 100644 src/components/ebay-area-chart/examples/data.json create mode 100644 src/components/ebay-area-chart/index.marko create mode 100644 src/components/ebay-area-chart/marko-tag.json create mode 100644 src/components/ebay-area-chart/style.less create mode 100644 src/components/ebay-bar-chart/README.md create mode 100644 src/components/ebay-bar-chart/bar-chart.stories.js create mode 100644 src/components/ebay-bar-chart/component.js create mode 100644 src/components/ebay-bar-chart/examples/data.json create mode 100644 src/components/ebay-bar-chart/index.marko create mode 100644 src/components/ebay-bar-chart/marko-tag.json create mode 100644 src/components/ebay-bar-chart/style.less create mode 100644 src/components/ebay-bar-chart/subtemplate.marko create mode 100644 src/components/ebay-line-chart/README.md create mode 100644 src/components/ebay-line-chart/component.js create mode 100644 src/components/ebay-line-chart/examples/data.json create mode 100644 src/components/ebay-line-chart/index.marko create mode 100644 src/components/ebay-line-chart/line-chart.stories.js create mode 100644 src/components/ebay-line-chart/marko-tag.json create mode 100644 src/components/ebay-line-chart/style.less create mode 100644 src/components/ebay-line-chart/tooltip.marko create mode 100644 src/components/ebay-spark-line/README.md create mode 100644 src/components/ebay-spark-line/component.js create mode 100644 src/components/ebay-spark-line/examples/data.json create mode 100644 src/components/ebay-spark-line/index.marko create mode 100644 src/components/ebay-spark-line/spark-line.stories.js create mode 100644 src/components/ebay-spark-line/style.js create mode 100644 src/components/ebay-spark-line/style.less create mode 100644 src/components/ebay-spark-line/test/__snapshots__/renders-a-blue-line.expected.html create mode 100644 src/components/ebay-spark-line/test/__snapshots__/renders-a-green-line.expected.html create mode 100644 src/components/ebay-spark-line/test/__snapshots__/renders-a-red-line.expected.html create mode 100644 src/components/ebay-spark-line/test/test.server.js diff --git a/LICENSE.txt b/LICENSE.txt index fdf8e6886..b51d2d6be 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -19,3 +19,11 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +USE OF SOME COMPONENTS REQUIRES A SEPARATE, NON-OPEN-SOURCE LICENSE FROM THIRD PARTIES + +The data visualization components and the charting components of the eBayUI library are designed to use one or more HighCharts® software products. HighCharts® is a registered trademark of HighSoft AS. HighSoft AS is not affiliated with Ebay. Ebay provides no warranties of any kind (e.g., of merchantability, fitness for a particular purpose, and noninfringement), whether express or implied, with respect to the HighCharts® software products that the data visualization components and the charting components are designed to use. + +COMMERCIAL USE OF HIGHCHARTS® SOFTWARE PRODUCTS REQUIRES A PAID LICENSE PROVIDED BY HIGHSOFT AS. While many components of the eBayUI library are licensed under the MIT License, the HighCharts® software products which the data visualization components and charting components of the EbayUI library are designed to use are NOT licensed under the MIT License or any other open source license. Rights pertaining to HighCharts® software products (e.g., including, but not limited to, rights to use, install, distribute, publish, merge, duplicate, and modify) are governed by the terms of one or more proprietary license agreements that are available online at http://www.highcharts.com or by the terms of custom license agreements that HighSoft AS may negotiate with its customers at its own discretion. While HighSoft AS may choose to license HighCharts® software products for non-commercial use at no cost, IT IS THE RESPONSIBILITY OF ANY PARTY THAT WISHES TO USE HIGHCHARTS® SOFTWARE PRODUCTS TO VERIFY THE TERMS OF SUCH A LICENSE WITH HIGHSOFT AS. NOTWITHSTANDING ANY PROVISION OF THIS LICENSE, PARTIES WHO ARE NOT LICENSED BY HIGHSOFT AS (OR ITS SUCCESSORS OR ASSIGNS) TO USE HIGHCHARTS® SOFTWARE PRODUCTS ARE NOT LICENSED TO USE THE DATA VISUALIZATION COMPONENTS AND THE CHARTING COMPONENTS OF THE EBAYUI LIBRARY. + +This notice shall be included in all copies or substantial portions of the Software. \ No newline at end of file diff --git a/README.md b/README.md index 107fe8ee2..a668b8b18 100644 --- a/README.md +++ b/README.md @@ -234,3 +234,11 @@ Looking to contribute to eBay UI? Please visit our [contributing page](CONTRIBUT Copyright (c) 2018 eBay Inc. Use of this source code is governed by a MIT-style license that can be found in the LICENSE file or at [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT). + +USE OF SOME COMPONENTS REQUIRES A SEPARATE, NON-OPEN-SOURCE LICENSE FROM THIRD PARTIES + +The data visualization components and the charting components of the eBayUI library are designed to use one or more HighCharts® software products. HighCharts® is a registered trademark of HighSoft AS. HighSoft AS is not affiliated with Ebay. Ebay provides no warranties of any kind (e.g., of merchantability, fitness for a particular purpose, and noninfringement), whether express or implied, with respect to the HighCharts® software products that the data visualization components and the charting components are designed to use. + +COMMERCIAL USE OF HIGHCHARTS® SOFTWARE PRODUCTS REQUIRES A PAID LICENSE PROVIDED BY HIGHSOFT AS. While many components of the eBayUI library are licensed under the MIT License, the HighCharts® software products which the data visualization components and charting components of the EbayUI library are designed to use are NOT licensed under the MIT License or any other open source license. Rights pertaining to HighCharts® software products (e.g., including, but not limited to, rights to use, install, distribute, publish, merge, duplicate, and modify) are governed by the terms of one or more proprietary license agreements that are available online at http://www.highcharts.com or by the terms of custom license agreements that HighSoft AS may negotiate with its customers at its own discretion. While HighSoft AS may choose to license HighCharts® software products for non-commercial use at no cost, IT IS THE RESPONSIBILITY OF ANY PARTY THAT WISHES TO USE HIGHCHARTS® SOFTWARE PRODUCTS TO VERIFY THE TERMS OF SUCH A LICENSE WITH HIGHSOFT AS. NOTWITHSTANDING ANY PROVISION OF THIS LICENSE, PARTIES WHO ARE NOT LICENSED BY HIGHSOFT AS (OR ITS SUCCESSORS OR ASSIGNS) TO USE HIGHCHARTS® SOFTWARE PRODUCTS ARE NOT LICENSED TO USE THE DATA VISUALIZATION COMPONENTS AND THE CHARTING COMPONENTS OF THE EBAYUI LIBRARY. + +This notice shall be included in all copies or substantial portions of the Software. diff --git a/package-lock.json b/package-lock.json index 4735884f3..0042b6867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@marko-tags/subscribe": "^0.4.2", + "highcharts": "^10.2.1", "makeup-active-descendant": "0.6.1", "makeup-expander": "~0.10.1", "makeup-floating-label": "~0.3.2", @@ -23774,6 +23775,11 @@ "he": "bin/he" } }, + "node_modules/highcharts": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-10.2.1.tgz", + "integrity": "sha512-4QwLQwWc0XdBHXc2Uy6IJisAUir+sgQIMyFqYZc3BD9iFSFUdllLkRyoed6y33aPb73LAMZE2qLB5SuLqFE/fg==" + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -58510,6 +58516,11 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "highcharts": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-10.2.1.tgz", + "integrity": "sha512-4QwLQwWc0XdBHXc2Uy6IJisAUir+sgQIMyFqYZc3BD9iFSFUdllLkRyoed6y33aPb73LAMZE2qLB5SuLqFE/fg==" + }, "highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", diff --git a/package.json b/package.json index 23f29be5d..5d5537171 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ }, "dependencies": { "@marko-tags/subscribe": "^0.4.2", + "highcharts": "^10.2.1", "makeup-active-descendant": "0.6.1", "makeup-expander": "~0.10.1", "makeup-floating-label": "~0.3.2", diff --git a/src/common/charts/bar-chart.js b/src/common/charts/bar-chart.js new file mode 100644 index 000000000..c5b86028f --- /dev/null +++ b/src/common/charts/bar-chart.js @@ -0,0 +1,131 @@ +export function eBayColumns(HighCharts) { + if (!HighCharts.seriesTypes.column.prototype.ebayColumn) { + // check if the column has been extended before attempting to extend again + HighCharts.wrap(HighCharts.seriesTypes.column.prototype, 'translate', function (proceed) { + // set a flag that can be checked so the prototype isn't overwritten twice, which looses the original code that is called with the proceed function + HighCharts.seriesTypes.column.prototype.ebayColumn = true; + const top = this.options.top, // pull out the top value from the highcharts options object + bottom = this.options.bottom; // pull out the bottom value from the highcarts options object + + // this runs the original code for this translate function at this point + // if it is not run HighCharts.each will not exist yet + proceed.call(this); + HighCharts.each(this.points, (point) => { + // loop over each data point element + const shapeArgs = point.shapeArgs, // reference to the points shapeArgs object + x = shapeArgs.x, // references to the shapeArgs X value + w = shapeArgs.width; // references to the shapeArgs width value + + let y = shapeArgs.y, // references to the shapeArgs X value + // references to the shapeArgs height value. + // If it is not marked as a bottom point subract 4 pixels to create the visual gap in the chart + h = shapeArgs.height - (bottom ? 0 : 4); + + // check to make sure h is not negative and if it is set the hight back to the original height and move it's y position instead + if (h < 0) { + h = shapeArgs.height; + y = y - 4; + } + + const cornerRadius = 3; + + // HighCharts.relativeLength returns a length based on either the integer value, or a percentage of a base with w being the base. + let rTopLeft = HighCharts.relativeLength(top ? cornerRadius : 0, w), + rTopRight = HighCharts.relativeLength(top ? cornerRadius : 0, w), + rBottomRight = HighCharts.relativeLength(bottom ? cornerRadius : 0, w), + rBottomLeft = HighCharts.relativeLength(bottom ? cornerRadius : 0, w); + + // max corner radius is half the width and height of the shape + const maxCornerRadius = Math.min(w, h) / 2; + + // adjust top left corner if it is larger that maxCornerRadius + if (rTopLeft > maxCornerRadius) rTopLeft = maxCornerRadius; + + // adjust top right corner if it is larger that maxCornerRadius + if (rTopRight > maxCornerRadius) rTopRight = maxCornerRadius; + + // adjust bottom right corner if it is larger that maxCornerRadius + if (rBottomRight > maxCornerRadius) rBottomRight = maxCornerRadius; + + // adjust bottom left corner if it is larger that maxCornerRadius + if (rBottomLeft > maxCornerRadius) rBottomLeft = maxCornerRadius; + + point.dlBox = point.shapeArgs; // set the data label Box for aligning tooltips to match the point shape + point.shapeY = y; // set the points y position + point.shapeType = 'path'; // set the shape type to path + point.shapeArgs = { + // define the shape arg used to render the svg path element + // d is a standard SVG path definition string + // refer to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + d: [ + // move to the top left corner plus rTopLeft for the beveled corner width to start the path + 'M', + x + rTopLeft, + y, + // top side line + 'L', + x + w - rTopRight, + y, + // top right corner curve + 'C', + // top right corner start bezier control point + x + w - rTopRight / 2, + y, + // top right corner end bezier control point + x + w, + y + rTopRight / 2, + // top right + x + w, + y + rTopRight, + // right side + 'L', + x + w, + y + h - rBottomRight, + // bottom right corner + 'C', + // bottom right corner start bezier control point + x + w, + y + h - rBottomRight / 2, + // bottom right corner end bezier control point + x + w - rBottomRight / 2, + y + h, + // bottom right + x + w - rBottomRight, + y + h, + // bottom side + 'L', + x + rBottomLeft, + y + h, + // bottom left corner + 'C', + // bottom left corner start bezier control point + x + rBottomLeft / 2, + y + h, + // bottom left corner start bezier control point + x, + y + h - rBottomLeft / 2, + // bottom left + x, + y + h - rBottomLeft, + // left side + 'L', + x, + y + rTopLeft, + // top left corner + 'C', + // top left corner start bezier control point + x, + y + rTopLeft / 2, + // top left corner end bezier control point + x + rTopLeft / 2, + y, + // top left corner + x + rTopLeft, + y, + 'Z', // close path + ], + }; + }); + }); + } +} diff --git a/src/common/charts/legend.js b/src/common/charts/legend.js new file mode 100644 index 000000000..018d7fe67 --- /dev/null +++ b/src/common/charts/legend.js @@ -0,0 +1,19 @@ +export function ebayLegend(H) { + H.wrap(H.Legend.prototype, 'colorizeItem', function (p, item, visible) { + // this helps make the legend svg elements render crisper + const width = H.pick(item.borderWidth, 1), + crisp = -(width % 2) / 2; + p.apply(this, [].slice.call(arguments, 1)); + + if (item.legendSymbol) { + if (visible) { + item.legendSymbol.attr({ + 'stroke-width': width, // set the border width if visible + translateX: crisp, // set translateX to land on a perfect pixel + translateY: crisp, // set translateX to land on a perfect pixel + stroke: item.options.borderColor, // set the border color of legend item + }); + } + } + }); +} diff --git a/src/common/charts/shared.js b/src/common/charts/shared.js new file mode 100644 index 000000000..c824c3473 --- /dev/null +++ b/src/common/charts/shared.js @@ -0,0 +1,79 @@ +export const chartFontFamily = '"Market Sans", Arial, sans-serif', + backgroundColor = 'var(--color-background-primary)', + gridColor = 'var(--color-data-viz-grid)', + labelsColor = 'var(--color-data-viz-labels)', + legendColor = 'var(--color-data-viz-legend)', + legendInactiveColor = 'var(--color-data-viz-legend-inactive)', + legendHoverColor = 'var(--color-data-viz-legend-hover)', + tooltipBackgroundColor = 'var(--color-neutral-0)', + tooltipShadows = + 'drop-shadow(0 2px 7px var(--color-data-viz-tooltip-shadow-primary)) drop-shadow(0 5px 7px var(--color-data-viz-tooltip-shadow-secondary))', + lineChartPrimaryColor = 'var(--color-data-viz-line-chart-primary)', + lineChartSecondaryColor = 'var(--color-data-viz-line-chart-secondary)', + lineChartTertiaryColor = 'var(--color-data-viz-line-chart-tertiary)', + lineChartQueternaryColor = 'var(--color-data-viz-line-chart-queternary)', + lineChartQuinaryColor = 'var(--color-data-viz-line-chart-quinary)', + trendPositiveColor = 'var(--color-data-viz-trend-positive)', + trendNegativeColor = 'var(--color-data-viz-trend-negative)', + chartPrimaryColor = 'var(--color-data-viz-chart-primary)', + chartSecondaryColor = 'var(--color-data-viz-chart-secondary)', + chartTertiaryBackgroundColor = 'var(--color-data-viz-chart-tertiary-background)', + chartTertiaryStrokeColor = 'var(--color-data-viz-chart-tertiary-stroke)', + chartQuaternaryBackgroundColor = 'var(--color-data-viz-chart-quaternary-background)', + chartQuaternaryStrokeColor = 'var(--color-data-viz-chart-quaternary-stroke)', + chartQuinaryBackgroundColor = 'var(--color-data-viz-chart-quinary-background)', + chartQuinaryStrokeColor = 'var(--color-data-viz-chart-quinary-stroke)', + // patterns are in highcharts PatternOptionsObject format + // refer to https://api.highcharts.com/class-reference/Highcharts.PatternOptionsObject + patternTertiary = { + pattern: { + path: { + // d is a standard SVG path definition string + // refer to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + d: 'M0 0 L0 3', // draw a 3 until vertical line + }, + width: 4.5, // defines the x bounds of the repeating pattern + height: 3, // defines the y bounds of the repeating pattern + backgroundColor: chartTertiaryBackgroundColor, // sets the patterns background color + color: chartTertiaryStrokeColor, // sets the patterns stroke color + patternTransform: 'rotate(-60)', // rotates the path -60 degrees + }, + }, + patternQuaternary = { + pattern: { + path: { + // d is a standard SVG path definition string + // refer to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + d: 'M0 0 L3 0', + }, + width: 3, // defines the x bounds of the repeating pattern + height: 5, // defines the y bounds of the repeating pattern + backgroundColor: chartQuaternaryBackgroundColor, // sets the patterns background color + color: chartQuaternaryStrokeColor, // sets the patterns stroke color + }, + }, + colorMapping = [ + chartPrimaryColor, + chartSecondaryColor, + patternTertiary, + patternQuaternary, + chartQuinaryBackgroundColor, + ], + // function is used to set up the colors including lineColor(svg stroke) on each of the series objects + // based on the length of the series array + setSeriesColors = function (series) { + const strokeColorMapping = [ + chartPrimaryColor, + chartSecondaryColor, + chartTertiaryStrokeColor, + chartQuaternaryStrokeColor, + chartQuaternaryStrokeColor, + ]; + + for (let i = 0; i < series.length; i++) { + // Added a modulus in case the user passes in more than 5 series so it doesn't error out + const color = strokeColorMapping[i % strokeColorMapping.length]; + series[i].lineColor = color; + series[i].borderColor = color; + } + }; diff --git a/src/common/event-utils/index.js b/src/common/event-utils/index.js index 6a68706b2..66f00a062 100644 --- a/src/common/event-utils/index.js +++ b/src/common/event-utils/index.js @@ -98,7 +98,18 @@ const resizeUtil = { removeEventListener, }; +function debounce(func, timeout = 100) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(this, args); + }, timeout); + }; +} + export { + debounce, handleEnterKeydown, handleActionKeydown, handleEscapeKeydown, diff --git a/src/components/ebay-area-chart/README.md b/src/components/ebay-area-chart/README.md new file mode 100644 index 000000000..779c8170d --- /dev/null +++ b/src/components/ebay-area-chart/README.md @@ -0,0 +1,10 @@ +

+ + ebay-area-chart + + + DS v3.7.0 + +

+ +The area chart displays one to five series of data points as an interactive stacked area chart diff --git a/src/components/ebay-area-chart/area-chart.stories.js b/src/components/ebay-area-chart/area-chart.stories.js new file mode 100644 index 000000000..f43caa5b5 --- /dev/null +++ b/src/components/ebay-area-chart/area-chart.stories.js @@ -0,0 +1,137 @@ +import { tagToString } from '../../../.storybook/storybook-code-source'; +import { addRenderBodies } from '../../../.storybook/utils'; +import Readme from './README.md'; +import Component from './index.marko'; +import * as sampleSeriesData from './examples/data.json'; + +const Template = (args) => ({ + input: addRenderBodies(args), +}); + +export default { + title: 'charts/ebay-area-chart', + excludeStories: '.*', + component: Component, + parameters: { + docs: { + description: { + component: Readme, + }, + }, + }, + argTypes: { + title: { + type: { name: 'string', required: false }, + description: 'A title displayed above the graph', + }, + description: { + type: { name: 'string', required: true }, + description: 'A description of what the chart is displaying', + }, + series: { + type: { name: 'object', required: true }, + description: + 'The series is an array of one to five arrays of point objects, each point contains an `x`, `y`, and `label`. `x` is an epoch/unix time code, `y` is a numeric value, `label` is what is displayed for the `y` value in the tool tip', + }, + xAxisLabelFormat: { + type: { name: 'string', required: false }, + description: + 'Used to modify the display of the x-axis labels. Accepts a string like `{value:%Y-%m-%d}`. Refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat for available format keys', + table: { + defaultValue: { + summary: '{value:%b %e}', + }, + }, + }, + xAxisPositioner: { + type: { name: 'function', required: false }, + description: + 'A custom function that returns an array of epoch/unix time values where x-axis labels will be displayed. You can access `this.dataMin` and `this.dataMax` from the function to help determine positions.', + }, + yAxisLabels: { + type: { name: 'array', required: false }, + description: + 'An array of labels to use on the y-axis. Use in conjunction with yAxisPositioner. Make sure the length of the yAxisLabels match the length of the positions array returned by the yAxisPositioner function', + }, + yAxisPositioner: { + type: { name: 'function', required: false }, + description: + 'A custom function that returns an array of numeric values where y-axis labels will be displayed. You can access `this.dataMin` and `this.dataMax` from the function to help determine positions', + }, + class: { + type: { name: 'string', require: false }, + description: 'A class name that will be added to the main chart container', + }, + }, +}; + +export const Standard = Template.bind({}); +Standard.args = { + title: 'Single series sample area chart', + description: 'this chart displays 30 days of sample values', + series: sampleSeriesData.slice(0, 1), +}; +Standard.parameters = { + docs: { + source: { + code: tagToString('bar-chart', Standard.args), + }, + }, +}; + +export const TwoSeries = Template.bind({}); +TwoSeries.args = { + title: 'Two series sample area chart', + description: 'this chart displays 30 days of values for sample1 and sample2', + series: sampleSeriesData.slice(0, 2), +}; +TwoSeries.parameters = { + docs: { + source: { + code: tagToString('bar-chart', TwoSeries.args), + }, + }, +}; + +export const ThreeSeries = Template.bind({}); +ThreeSeries.args = { + title: 'Three series sample area chart', + description: 'this chart displays 30 days of values for sample1, sample2 and sample3', + series: sampleSeriesData.slice(0, 3), +}; +ThreeSeries.parameters = { + docs: { + source: { + code: tagToString('bar-chart', ThreeSeries.args), + }, + }, +}; + +export const FourSeries = Template.bind({}); +FourSeries.args = { + title: 'Four series sample area chart', + description: 'this chart displays 30 days of values for sample1, sample2, sample3, and sample4', + series: sampleSeriesData.slice(0, 4), +}; +FourSeries.parameters = { + docs: { + source: { + code: tagToString('bar-chart', FourSeries.args), + }, + }, +}; + +export const FiveSeries = Template.bind({}); +FiveSeries.args = { + title: 'Five series sample area chart', + description: + 'this chart displays 30 days of values for sample1, sample2, sample3, sample4, and sample5', + series: sampleSeriesData, +}; +FiveSeries.parameters = { + docs: { + source: { + code: tagToString('bar-chart', FiveSeries.args), + }, + }, +}; diff --git a/src/components/ebay-area-chart/component.js b/src/components/ebay-area-chart/component.js new file mode 100644 index 000000000..6f9fc2523 --- /dev/null +++ b/src/components/ebay-area-chart/component.js @@ -0,0 +1,347 @@ +import Highcharts from 'highcharts'; +import accessibility from 'highcharts/modules/accessibility'; +import patternFill from 'highcharts/modules/pattern-fill'; +import { + chartFontFamily, + backgroundColor, + gridColor, + labelsColor, + legendColor, + legendInactiveColor, + legendHoverColor, + tooltipBackgroundColor, + tooltipShadows, + setSeriesColors, + colorMapping, +} from '../../common/charts/shared'; +import { debounce } from '../../common/event-utils'; +import { ebayLegend } from '../../common/charts/legend'; + +const pointSize = 1.5; + +export default class { + onMount() { + this._initializeHighchartExtensions(); + this._setupEvents(); + this._setupCharts(); + } + onInput() { + // if chartRef does not exist do not try to run setupCharts as it may be server side and highcharts only works on the client side + if (this.chartRef && this.chartRef.destroy) { + this.chartRef.destroy(); + this._setupCharts(); + } + } + getContainerId() { + return `ebay-bar-chart-${this.id}`; + } + _initializeHighchartExtensions() { + // enable highcharts accessibility with wrapper function + accessibility(Highcharts); + // patternFill highcharts wrapper function enables rendering patterns instead of just solid colors + patternFill(Highcharts); + // add custom legend wrapper function + ebayLegend(Highcharts); + } + _setupEvents() { + // bind functions to keep scope and setup debounced versions of function calls + this.debounce = debounce.bind(this); + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + this.mouseOut = this.debounce(() => this.handleMouseOut(), 80); // 80ms delay for debounce + this.mouseOver = this.debounce((e) => this.handleMouseOver(e), 85); // 85ms delay for debounce so it doesn't colide with mouseOut debounce calls + } + _setupCharts() { + // check if a single series was passed in for series and if so add it to a new array + const series = Array.isArray(this.input.series) ? this.input.series : [this.input.series]; + + // update the zIndex of each series object so they render in the correct order + // and configure the markers that are displayed on hover + series.forEach((s, i) => { + s.zIndex = series.length - i; + s.marker = { + symbol: 'circle', + borderWidth: 3, + }; + }); + setSeriesColors(series); + + const config = { + title: this.getTitleConfig(), + chart: this.getChartConfig(), + colors: colorMapping, + xAxis: this.getXAxisConfig(), + yAxis: this.getYAxisConfig(series), + legend: this.getLegendConfig(), + tooltip: this.getTooltipConfig(), + plotOptions: this.getPlotOptions(), + series, // pass in the configured series array + credits: { + enabled: false, // hide the highcharts label and link in the bottom right of chart + }, + }; + // initialize and keep reference to chart + this.chartRef = Highcharts.chart(this.getContainerId(), config); + } + getTitleConfig() { + return { + text: this.input.title, + align: 'left', + useHTML: true, + style: { + // styles are set in JS since they are rendered in the SVG + fontSize: '18px', + fontWeight: 700, + }, + }; + } + getChartConfig() { + return { + type: 'area', + backgroundColor: backgroundColor, + style: { + // styles are set in JS since they are rendered in the SVG + fontFamily: chartFontFamily, + }, + }; + } + getXAxisConfig() { + return { + // currently setup to support epoch time values for xAxisLabels. + // It is possible to set custom non datetime xAxisLabels but will need changes to this component + type: 'datetime', + labels: { + // input.xAxisLabelFormat allows overriding the default short month / day label + // refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat to customize + format: this.input.xAxisLabelFormat ? this.input.xAxisLabelFormat : '{value:%b %e}', + align: 'center', + style: { + color: labelsColor, // setting label colors + }, + }, + tickWidth: 0, // hide the vertical tick on xAxis labels + crosshair: { + zIndex: 3, // make sure the vertical crosshair line on hover shows up on top + }, + tickPositioner: this.input.xAxisPositioner, // optional input to allow configuring the position of xAxis tick marks + }; + } + getYAxisConfig(series) { + const component = this; // component reference used in formatter functions that don't have the same scope + let yLabelsItterator = 0; // used when yAxisLabels array is provided in input + let maxYAxisValue = 0; // use to determine the highest yAxis value + series.forEach((s) => { + maxYAxisValue = s.data.reduce((p, c) => (c > p ? c : p), maxYAxisValue); + }); + return { + gridLineColor: gridColor, // sets the horizontal grid line colors + opposite: true, // moves yAxis labels to the right side of the chart + reversedStacks: false, // makes so series one starts at the bottom of the yAxis, by default this is true + labels: { + // if yAxisLabels are not passed in display the standard label + format: !this.input.yAxisLabels && '${text}', + // if yAxisLabels array is passed in this formatter function is needed to + // return the proper label for each yAxis tick mark + formatter: + this.input.yAxisLabels && + function () { + if (this.isFirst) { + yLabelsItterator = -1; + } + yLabelsItterator = yLabelsItterator + 1; + return component.input.yAxisLabels[yLabelsItterator]; + }, + style: { + color: labelsColor, // setting label colors + }, + }, + maxVal: maxYAxisValue, + title: { + enabled: false, // hide the axis label next to the axis + }, + offset: 0, // set to zero for no offset refer to https://api.highcharts.com/highcharts/yAxis.offset + // passed in function for yAxisPositioner refer to https://api.highcharts.com/highcharts/yAxis.tickPositioner for use + tickPositioner: this.input.yAxisPositioner, + }; + } + getLegendConfig() { + return { + // if only a single series is provided do not display the legend + enabled: this.input.series.length > 1, + symbolRadius: 2, // corner radius on legend identifiers svg element + symbolWidth: 12, // setting the width of the legend identifiers svg element + symbolHeight: 12, // setting the height of the legend identifiers svg element + itemStyle: { + color: legendColor, // set the color of the text in the legend + }, + itemHiddenStyle: { + color: legendInactiveColor, // set legend text color when legend item has been clicked and hidden + }, + itemHoverStyle: { + color: legendHoverColor, // set legend text color on hover of legend element + }, + }; + } + + getTooltipConfig() { + const component = this; // component reference used in formatter functions that don't have the same scope + return { + formatter: function () { + // refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat for dateFormat variables + // s is used to compile html string of formatted tooltip data + let s = `${Highcharts.dateFormat('%b %e, %Y', this.x, false)}
`; // sets the displayed date at the top of the tooltip + if (component.chartRef.series.length > 1) { + // setup html for multi series tooltip + component.chartRef.series.forEach((serie) => { + // cycle through each series + serie.data.forEach((d) => { + // cycle through each series data array to match x value with active hovered xAxis position + if (d.x === this.x) { + // when the x value matches the hovered xAxis position + s += `
${serie.name}${d.label}
`; + } + }); + }); + } else { + // setup html for single series tooltip + // cycle through points of the single series and find the one that matches the active xAxis + this.points.forEach((d) => { + if (d.x === this.x) { + // when the x value matches the hovered xAxis position + s += `
${d.point.label}
`; + } + }); + } + return s; + }, + useHTML: true, // allows defining html to format tooltip content + backgroundColor: tooltipBackgroundColor, // sets tooltip background color + borderWidth: 0, // hide the default border stroke + borderRadius: 10, // set the border radius of the tooltip + outside: true, // used to render the tooltip outside of the main SVG element + shadow: false, // hide the default shadow as it conflicts with designs + shared: true, // shared means that if there are multipe series passed in there will be a single tooltip element per xAxis point + style: { + filter: tooltipShadows, // sets tooltip shadows + fontSize: '12px', + }, + }; + } + + getPlotOptions() { + return { + series: { + description: this.input.description, // set the description that was passed in + // config stacking to normal to make sure series stack without overlapping + // refer to https://api.highcharts.com/highcharts/plotOptions.area.stacking + stacking: 'normal', + point: { + // assign mouse events to point hovers + events: { + mouseOver: this.mouseOver, + mouseOut: this.mouseOut, + }, + }, + }, + area: { + className: 'ebay-area-chart', // add class to area chart to allow targetted styles from style.less file + lineWidth: 1, // set the border line width for each series item. + // states: { // set if we do not want series to fade out on legend hover uncomment this block + // inactive: { + // opacity: 1, + // } + // } + }, + }; + } + + // debounce used to help improve performance on mouse interactions + debounce(func, timeout = 100) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(this, args); + }, timeout); + }; + } + handleMouseOut() { + // this function is debounced to improve performance + this.chartRef.series.forEach((s) => { + s.data.forEach((d) => { + // check if hover is on the xAxis (onTick) for each item, + // and if they have a className remove and disable the marker + if (!d.onTick && d.className !== null) { + d.update( + { + className: null, // nullify className if not active + marker: { + enabled: false, // disable marker if not active + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } else if (d.onTick && d.className === null) { + d.update( + { + className: 'ebay-area-chart__marker--visible', // set classname + onTick: d.onTick, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: true, // set marker enabled + radius: pointSize, // set the size of marker + lineColor: backgroundColor, // set border color of hover markers + lineWidth: 4, // set border width of hover markers + fillColor: '#000000', // set fill color of markers + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } + }); + }); + this.chartRef.redraw(); // trigger redraw after all points have been updated + } + handleMouseOver(e) { + // this function is debounced to improve performance + this.chartRef.series.forEach((s) => { + s.data.forEach((d) => { + if (d.x === e.target.x) { + // if active xAxis hover position matches the data point x update the marker to display + d.update( + { + className: 'ebay-area-chart__marker--visible', // sets the classname + onTick: d.onTick, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: true, // set marker enabled + radius: pointSize, // set the size of marker + lineColor: backgroundColor, // set border color of hover markers + lineWidth: 4, // set border width of hover markers + fillColor: '#000000', // set fill color of markers + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } else if (!d.onTick && d.className !== null) { + d.update( + { + className: null, // nullify className if not active + onTick: d.onTick, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: false, // disable marker + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } + }); + }); + this.chartRef.redraw(); // trigger redraw after all points have been updated + } + onDestroy() { + this.chartRef.destroy(); + } +} diff --git a/src/components/ebay-area-chart/examples/data.json b/src/components/ebay-area-chart/examples/data.json new file mode 100644 index 000000000..207532399 --- /dev/null +++ b/src/components/ebay-area-chart/examples/data.json @@ -0,0 +1,777 @@ +[ + { + "data": [ + { + "x": 1643673600000, + "y": 405.54, + "label": "$405.54" + }, + { + "x": 1643760000000, + "y": 3529.13, + "label": "$3529.13" + }, + { + "x": 1643846400000, + "y": 7290.99, + "label": "$7290.99" + }, + { + "x": 1643932800000, + "y": 7598.54, + "label": "$7598.54" + }, + { + "x": 1644019200000, + "y": 6014.85, + "label": "$6014.85" + }, + { + "x": 1644105600000, + "y": 7197.59, + "label": "$7197.59" + }, + { + "x": 1644192000000, + "y": 7054.97, + "label": "$7054.97" + }, + { + "x": 1644278400000, + "y": 8813.92, + "label": "$8813.92" + }, + { + "x": 1644364800000, + "y": 11303.55, + "label": "$11303.55" + }, + { + "x": 1644451200000, + "y": 11425.54, + "label": "$11425.54" + }, + { + "x": 1644537600000, + "y": 14767.84, + "label": "$14767.84" + }, + { + "x": 1644624000000, + "y": 14595.77, + "label": "$14595.77" + }, + { + "x": 1644710400000, + "y": 14896.37, + "label": "$14896.37" + }, + { + "x": 1644796800000, + "y": 18686.42, + "label": "$18686.42" + }, + { + "x": 1644883200000, + "y": 19397.06, + "label": "$19397.06" + }, + { + "x": 1644969600000, + "y": 17377.38, + "label": "$17377.38" + }, + { + "x": 1645056000000, + "y": 19762.2, + "label": "$19762.2" + }, + { + "x": 1645142400000, + "y": 22693.32, + "label": "$22693.32" + }, + { + "x": 1645228800000, + "y": 25474.69, + "label": "$25474.69" + }, + { + "x": 1645315200000, + "y": 25716.96, + "label": "$25716.96" + }, + { + "x": 1645401600000, + "y": 28229.64, + "label": "$28229.64" + }, + { + "x": 1645488000000, + "y": 30802.49, + "label": "$30802.49" + }, + { + "x": 1645574400000, + "y": 30784.09, + "label": "$30784.09" + }, + { + "x": 1645660800000, + "y": 31268.12, + "label": "$31268.12" + }, + { + "x": 1645747200000, + "y": 33762.26, + "label": "$33762.26" + }, + { + "x": 1645833600000, + "y": 35829.81, + "label": "$35829.81" + }, + { + "x": 1645920000000, + "y": 35459.05, + "label": "$35459.05" + }, + { + "x": 1646006400000, + "y": 38357.93, + "label": "$38357.93" + }, + { + "x": 1646092800000, + "y": 41968.57, + "label": "$41968.57" + }, + { + "x": 1646179200000, + "y": 43848.26, + "label": "$43848.26" + } + ], + "name": "Value 1" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 2191.95, + "label": "$2191.95" + }, + { + "x": 1643760000000, + "y": 2401.84, + "label": "$2401.84" + }, + { + "x": 1643846400000, + "y": 2212.28, + "label": "$2212.28" + }, + { + "x": 1643932800000, + "y": 2608.39, + "label": "$2608.39" + }, + { + "x": 1644019200000, + "y": 2000.86, + "label": "$2000.86" + }, + { + "x": 1644105600000, + "y": 3931.71, + "label": "$3931.71" + }, + { + "x": 1644192000000, + "y": 4094.93, + "label": "$4094.93" + }, + { + "x": 1644278400000, + "y": 7666.27, + "label": "$7666.27" + }, + { + "x": 1644364800000, + "y": 6413, + "label": "$6413" + }, + { + "x": 1644451200000, + "y": 9924.87, + "label": "$9924.87" + }, + { + "x": 1644537600000, + "y": 11485.98, + "label": "$11485.98" + }, + { + "x": 1644624000000, + "y": 13416.08, + "label": "$13416.08" + }, + { + "x": 1644710400000, + "y": 16068.24, + "label": "$16068.24" + }, + { + "x": 1644796800000, + "y": 15855.76, + "label": "$15855.76" + }, + { + "x": 1644883200000, + "y": 19313.94, + "label": "$19313.94" + }, + { + "x": 1644969600000, + "y": 20328.31, + "label": "$20328.31" + }, + { + "x": 1645056000000, + "y": 21482.19, + "label": "$21482.19" + }, + { + "x": 1645142400000, + "y": 24732.07, + "label": "$24732.07" + }, + { + "x": 1645228800000, + "y": 26000.25, + "label": "$26000.25" + }, + { + "x": 1645315200000, + "y": 28788.89, + "label": "$28788.89" + }, + { + "x": 1645401600000, + "y": 30438.55, + "label": "$30438.55" + }, + { + "x": 1645488000000, + "y": 33025.77, + "label": "$33025.77" + }, + { + "x": 1645574400000, + "y": 33232.63, + "label": "$33232.63" + }, + { + "x": 1645660800000, + "y": 35412.85, + "label": "$35412.85" + }, + { + "x": 1645747200000, + "y": 35418.1, + "label": "$35418.1" + }, + { + "x": 1645833600000, + "y": 34787.87, + "label": "$34787.87" + }, + { + "x": 1645920000000, + "y": 35379.97, + "label": "$35379.97" + }, + { + "x": 1646006400000, + "y": 35058.06, + "label": "$35058.06" + }, + { + "x": 1646092800000, + "y": 36941.95, + "label": "$36941.95" + }, + { + "x": 1646179200000, + "y": 35433.32, + "label": "$35433.32" + } + ], + "name": "Value 2" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 0, + "label": "$0" + }, + { + "x": 1643760000000, + "y": 1972.03, + "label": "$1972.03" + }, + { + "x": 1643846400000, + "y": 2605.21, + "label": "$2605.21" + }, + { + "x": 1643932800000, + "y": 2688.03, + "label": "$2688.03" + }, + { + "x": 1644019200000, + "y": 3693.2, + "label": "$3693.2" + }, + { + "x": 1644105600000, + "y": 6620.37, + "label": "$6620.37" + }, + { + "x": 1644192000000, + "y": 9938.94, + "label": "$9938.94" + }, + { + "x": 1644278400000, + "y": 9041.09, + "label": "$9041.09" + }, + { + "x": 1644364800000, + "y": 8988.36, + "label": "$8988.36" + }, + { + "x": 1644451200000, + "y": 10127.2, + "label": "$10127.2" + }, + { + "x": 1644537600000, + "y": 13612.53, + "label": "$13612.53" + }, + { + "x": 1644624000000, + "y": 14706.86, + "label": "$14706.86" + }, + { + "x": 1644710400000, + "y": 13564.87, + "label": "$13564.87" + }, + { + "x": 1644796800000, + "y": 12415.62, + "label": "$12415.62" + }, + { + "x": 1644883200000, + "y": 16964.94, + "label": "$16964.94" + }, + { + "x": 1644969600000, + "y": 18740.34, + "label": "$18740.34" + }, + { + "x": 1645056000000, + "y": 18716.07, + "label": "$18716.07" + }, + { + "x": 1645142400000, + "y": 21154.56, + "label": "$21154.56" + }, + { + "x": 1645228800000, + "y": 21750.07, + "label": "$21750.07" + }, + { + "x": 1645315200000, + "y": 22922.59, + "label": "$22922.59" + }, + { + "x": 1645401600000, + "y": 26768.79, + "label": "$26768.79" + }, + { + "x": 1645488000000, + "y": 26399.77, + "label": "$26399.77" + }, + { + "x": 1645574400000, + "y": 29910.73, + "label": "$29910.73" + }, + { + "x": 1645660800000, + "y": 32231.86, + "label": "$32231.86" + }, + { + "x": 1645747200000, + "y": 34040.09, + "label": "$34040.09" + }, + { + "x": 1645833600000, + "y": 31844.54, + "label": "$31844.54" + }, + { + "x": 1645920000000, + "y": 34033.48, + "label": "$34033.48" + }, + { + "x": 1646006400000, + "y": 34140.3, + "label": "$34140.3" + }, + { + "x": 1646092800000, + "y": 33203.36, + "label": "$33203.36" + }, + { + "x": 1646179200000, + "y": 35918.18, + "label": "$35918.18" + } + ], + "name": "Value 3" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 3714.81, + "label": "$3714.81" + }, + { + "x": 1643760000000, + "y": 3515.59, + "label": "$3515.59" + }, + { + "x": 1643846400000, + "y": 5668.89, + "label": "$5668.89" + }, + { + "x": 1643932800000, + "y": 7707.67, + "label": "$7707.67" + }, + { + "x": 1644019200000, + "y": 7065.82, + "label": "$7065.82" + }, + { + "x": 1644105600000, + "y": 10961.2, + "label": "$10961.2" + }, + { + "x": 1644192000000, + "y": 10810.1, + "label": "$10810.1" + }, + { + "x": 1644278400000, + "y": 9299.88, + "label": "$9299.88" + }, + { + "x": 1644364800000, + "y": 12175.1, + "label": "$12175.1" + }, + { + "x": 1644451200000, + "y": 15678.8, + "label": "$15678.8" + }, + { + "x": 1644537600000, + "y": 13403.74, + "label": "$13403.74" + }, + { + "x": 1644624000000, + "y": 14934.49, + "label": "$14934.49" + }, + { + "x": 1644710400000, + "y": 18195.71, + "label": "$18195.71" + }, + { + "x": 1644796800000, + "y": 21630.27, + "label": "$21630.27" + }, + { + "x": 1644883200000, + "y": 19205.48, + "label": "$19205.48" + }, + { + "x": 1644969600000, + "y": 18236.22, + "label": "$18236.22" + }, + { + "x": 1645056000000, + "y": 20057.18, + "label": "$20057.18" + }, + { + "x": 1645142400000, + "y": 21599.02, + "label": "$21599.02" + }, + { + "x": 1645228800000, + "y": 22092.15, + "label": "$22092.15" + }, + { + "x": 1645315200000, + "y": 22049.95, + "label": "$22049.95" + }, + { + "x": 1645401600000, + "y": 25488.9, + "label": "$25488.9" + }, + { + "x": 1645488000000, + "y": 23560.32, + "label": "$23560.32" + }, + { + "x": 1645574400000, + "y": 23287.49, + "label": "$23287.49" + }, + { + "x": 1645660800000, + "y": 23291.42, + "label": "$23291.42" + }, + { + "x": 1645747200000, + "y": 22578.91, + "label": "$22578.91" + }, + { + "x": 1645833600000, + "y": 23359.51, + "label": "$23359.51" + }, + { + "x": 1645920000000, + "y": 26612.42, + "label": "$26612.42" + }, + { + "x": 1646006400000, + "y": 27406.05, + "label": "$27406.05" + }, + { + "x": 1646092800000, + "y": 26739.38, + "label": "$26739.38" + }, + { + "x": 1646179200000, + "y": 31025.67, + "label": "$31025.67" + } + ], + "name": "Value 4" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 2634.55, + "label": "$2634.55" + }, + { + "x": 1643760000000, + "y": 5550.37, + "label": "$5550.37" + }, + { + "x": 1643846400000, + "y": 8362.35, + "label": "$8362.35" + }, + { + "x": 1643932800000, + "y": 8892.21, + "label": "$8892.21" + }, + { + "x": 1644019200000, + "y": 8832.23, + "label": "$8832.23" + }, + { + "x": 1644105600000, + "y": 11513.81, + "label": "$11513.81" + }, + { + "x": 1644192000000, + "y": 10620.6, + "label": "$10620.6" + }, + { + "x": 1644278400000, + "y": 10006.53, + "label": "$10006.53" + }, + { + "x": 1644364800000, + "y": 11226.84, + "label": "$11226.84" + }, + { + "x": 1644451200000, + "y": 12663.56, + "label": "$12663.56" + }, + { + "x": 1644537600000, + "y": 13521.25, + "label": "$13521.25" + }, + { + "x": 1644624000000, + "y": 17157.52, + "label": "$17157.52" + }, + { + "x": 1644710400000, + "y": 17379.93, + "label": "$17379.93" + }, + { + "x": 1644796800000, + "y": 18145.16, + "label": "$18145.16" + }, + { + "x": 1644883200000, + "y": 18417.28, + "label": "$18417.28" + }, + { + "x": 1644969600000, + "y": 20505.13, + "label": "$20505.13" + }, + { + "x": 1645056000000, + "y": 23222.51, + "label": "$23222.51" + }, + { + "x": 1645142400000, + "y": 22161.92, + "label": "$22161.92" + }, + { + "x": 1645228800000, + "y": 24232.12, + "label": "$24232.12" + }, + { + "x": 1645315200000, + "y": 25948.31, + "label": "$25948.31" + }, + { + "x": 1645401600000, + "y": 26334.97, + "label": "$26334.97" + }, + { + "x": 1645488000000, + "y": 27880.25, + "label": "$27880.25" + }, + { + "x": 1645574400000, + "y": 26813.33, + "label": "$26813.33" + }, + { + "x": 1645660800000, + "y": 29132.31, + "label": "$29132.31" + }, + { + "x": 1645747200000, + "y": 28896.25, + "label": "$28896.25" + }, + { + "x": 1645833600000, + "y": 30834.8, + "label": "$30834.8" + }, + { + "x": 1645920000000, + "y": 29546.97, + "label": "$29546.97" + }, + { + "x": 1646006400000, + "y": 31693.82, + "label": "$31693.82" + }, + { + "x": 1646092800000, + "y": 31394.2, + "label": "$31394.2" + }, + { + "x": 1646179200000, + "y": 34090.57, + "label": "$34090.57" + } + ], + "name": "Value 5" + } +] diff --git a/src/components/ebay-area-chart/index.marko b/src/components/ebay-area-chart/index.marko new file mode 100644 index 000000000..ae7f0a726 --- /dev/null +++ b/src/components/ebay-area-chart/index.marko @@ -0,0 +1,22 @@ +import { processHtmlAttributes } from "../../common/html-attributes" +static var ignoredAttributes = [ + 'data', + 'class', + 'series', + 'title', + 'description', + 'xAxisLabelFormat', + 'yAxisLabels', + 'yAxisLabels', + 'xAxisPositioner', + 'yAxisPositioner' +]; + +
+ +
diff --git a/src/components/ebay-area-chart/marko-tag.json b/src/components/ebay-area-chart/marko-tag.json new file mode 100644 index 000000000..2f5bee5f0 --- /dev/null +++ b/src/components/ebay-area-chart/marko-tag.json @@ -0,0 +1,22 @@ +{ + "attribute-groups": ["html-attributes"], + "@*": { + "targetProperty": null, + "type": "expression" + }, + "@html-attributes": "expression", + "@title": "string", + "@description": "string", + "@series[]": { + "@title": "string", + "@data[]": { + "@x": "number", + "@y": "number", + "@label": "string" + } + }, + "@xAxisLabelFormat": "string", + "@xAxisPositioner": "function", + "@yAxisLabels": "array", + "@yAxisPositioner": "function" +} diff --git a/src/components/ebay-area-chart/style.less b/src/components/ebay-area-chart/style.less new file mode 100644 index 000000000..e237ac012 --- /dev/null +++ b/src/components/ebay-area-chart/style.less @@ -0,0 +1,23 @@ +.ebay-area-chart { + .highcharts-point { + opacity: 0; // overriding the default highcharts opacity to 0 + stroke-width: 2px; // and adjusting the stroke width + } + + .ebay-area-chart__marker--visible { + opacity: 1; + } + + .highcharts-legend-item { + .highcharts-point { + opacity: 1; // displays the legend marker symbols which are hidden by the highcharts-point opacity + stroke-width: 1; // sets the stroke width on the plot point markers + } + } + + @media (prefers-color-scheme: dark) { + .highcharts-halo { + fill-opacity: 0.8; // this inverses the default 0.2 fill opacity in from light mode + } + } +} diff --git a/src/components/ebay-bar-chart/README.md b/src/components/ebay-bar-chart/README.md new file mode 100644 index 000000000..14a05ec4b --- /dev/null +++ b/src/components/ebay-bar-chart/README.md @@ -0,0 +1,10 @@ +

+ + ebay-bar-chart + + + DS v3.7.0 + +

+ +The bar chart displays one to five series of data points as an interactive bar chart. The bars can be side by side, or stacked. diff --git a/src/components/ebay-bar-chart/bar-chart.stories.js b/src/components/ebay-bar-chart/bar-chart.stories.js new file mode 100644 index 000000000..7aab74f48 --- /dev/null +++ b/src/components/ebay-bar-chart/bar-chart.stories.js @@ -0,0 +1,229 @@ +import { tagToString } from '../../../.storybook/storybook-code-source'; +import { addRenderBodies } from '../../../.storybook/utils'; +import Readme from './README.md'; +import Component from './index.marko'; +import * as sampleSeriesData from './examples/data.json'; +const Template = (args) => ({ + input: addRenderBodies(args), +}); + +export default { + title: 'charts/ebay-bar-chart', + excludeStories: '.*', + component: Component, + parameters: { + docs: { + description: { + component: Readme, + }, + }, + }, + argTypes: { + title: { + type: { name: 'string', required: false }, + description: 'A title displayed above the graph', + }, + description: { + type: { name: 'string', required: true }, + description: 'A description of what the chart is displaying', + }, + series: { + type: { name: 'object', required: true }, + description: + 'The series is an array of one to five arrays of point objects, each point contains an `x`, `y`, and `label`. `x` is an epoch/unix time code, `y` is a numeric value, `label` is what is displayed for the `y` value in the tool tip', + }, + xAxisLabelFormat: { + type: { name: 'string', required: false }, + description: + 'Used to modify the display of the x-axis labels. Accepts a string like `{value:%Y-%m-%d}`. Refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat for available format keys', + table: { + defaultValue: { + summary: '{value:%b %e}', + }, + }, + }, + xAxisPositioner: { + type: { name: 'function', required: false }, + description: + 'A custom function that returns an array of epoch/unix time values where x-axis labels will be displayed. You can access `this.dataMin` and `this.dataMax` from the function to help determine positions.', + }, + yAxisLabels: { + type: { name: 'array', required: false }, + description: + 'An array of labels to use on the y-axis. Use in conjunction with yAxisPositioner. Make sure the length of the yAxisLabels match the length of the positions array returned by the yAxisPositioner function', + }, + yAxisPositioner: { + type: { name: 'function', required: false }, + description: + 'A custom function that returns an array of numeric values where y-axis labels will be displayed. You can access `this.dataMin` and `this.dataMax` from the function to help determine positions', + }, + stacked: { + type: { name: 'boolean', required: false }, + description: 'Stacked can bet set to `true` or `false` and defaults to false', + table: { + defaultValue: { + summary: 'false', + }, + }, + }, + class: { + type: { name: 'string', require: false }, + description: 'A class name that will be added to the main chart container', + }, + }, +}; + +function getSeriesData(series, days) { + return sampleSeriesData.slice(0, series).map((s) => ({ + ...s, + data: s.data.slice(0, days), + })); +} + +export const singleSeriesFiveDays = Template.bind({}); +singleSeriesFiveDays.args = { + series: getSeriesData(1, 5), +}; +singleSeriesFiveDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', singleSeriesFiveDays.args), + }, + }, +}; + +export const singleSeriesThirteenDays = Template.bind({}); +singleSeriesThirteenDays.args = { + series: getSeriesData(1, 13), +}; +singleSeriesThirteenDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', singleSeriesThirteenDays.args), + }, + }, +}; + +export const singleSeriesThirtyDays = Template.bind({}); +singleSeriesThirtyDays.args = { + series: getSeriesData(1, 30), +}; +singleSeriesThirtyDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', singleSeriesThirtyDays.args), + }, + }, +}; + +export const twoSeriesEightDays = Template.bind({}); +twoSeriesEightDays.args = { + series: getSeriesData(2, 8), +}; +twoSeriesEightDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', twoSeriesEightDays.args), + }, + }, +}; + +export const threeSeriesThreeDays = Template.bind({}); +threeSeriesThreeDays.args = { + series: getSeriesData(3, 3), +}; +threeSeriesThreeDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', threeSeriesThreeDays.args), + }, + }, +}; + +export const fourSeriesFourDays = Template.bind({}); +fourSeriesFourDays.args = { + series: getSeriesData(4, 4), +}; +fourSeriesFourDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', fourSeriesFourDays.args), + }, + }, +}; + +export const fiveSeriesThreeDays = Template.bind({}); +fiveSeriesThreeDays.args = { + series: getSeriesData(5, 3), +}; +fiveSeriesThreeDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', fiveSeriesThreeDays.args), + }, + }, +}; + +export const fiveSeriesSixDays = Template.bind({}); +fiveSeriesSixDays.args = { + series: getSeriesData(5, 6), +}; +fiveSeriesSixDays.parameters = { + docs: { + source: { + code: tagToString('bar-chart', fiveSeriesSixDays.args), + }, + }, +}; + +export const twoSeriesStacked = Template.bind({}); +twoSeriesStacked.args = { + series: getSeriesData(2, 24), + stacked: true, +}; +twoSeriesStacked.parameters = { + docs: { + source: { + code: tagToString('bar-chart', twoSeriesStacked.args), + }, + }, +}; + +export const threeSeriesStacked = Template.bind({}); +threeSeriesStacked.args = { + series: getSeriesData(3, 24), + stacked: true, +}; +threeSeriesStacked.parameters = { + docs: { + source: { + code: tagToString('bar-chart', threeSeriesStacked.args), + }, + }, +}; + +export const fourSeriesStacked = Template.bind({}); +fourSeriesStacked.args = { + series: getSeriesData(4, 24), + stacked: true, +}; +fourSeriesStacked.parameters = { + docs: { + source: { + code: tagToString('bar-chart', fourSeriesStacked.args), + }, + }, +}; + +export const fiveSeriesStacked = Template.bind({}); +fiveSeriesStacked.args = { + series: getSeriesData(5, 24), + stacked: true, +}; +fiveSeriesStacked.parameters = { + docs: { + source: { + code: tagToString('bar-chart', fiveSeriesStacked.args), + }, + }, +}; diff --git a/src/components/ebay-bar-chart/component.js b/src/components/ebay-bar-chart/component.js new file mode 100644 index 000000000..8119177aa --- /dev/null +++ b/src/components/ebay-bar-chart/component.js @@ -0,0 +1,359 @@ +import Highcharts from 'highcharts'; +import accessibility from 'highcharts/modules/accessibility'; +import patternFill from 'highcharts/modules/pattern-fill'; + +import { + chartFontFamily, + backgroundColor, + gridColor, + labelsColor, + legendColor, + legendInactiveColor, + legendHoverColor, + tooltipBackgroundColor, + tooltipShadows, + setSeriesColors, + colorMapping, +} from '../../common/charts/shared'; + +import { ebayLegend } from '../../common/charts/legend'; +import { eBayColumns } from '../../common/charts/bar-chart'; + +import subtemplate from './subtemplate.marko'; + +export default class { + onMount() { + this._initializeHighchartsExtensions(); + this._setupCharts(); + } + onInput() { + // if chartRef does not exist do not try to run setupCharts as it may be server side and highcharts only works on the client side + if (this.chartRef && this.chartRef.destroy) { + this.chartRef.destroy(); + this._setupCharts(); + } + } + getContainerId() { + return `ebay-bar-chart-${this.id}`; + } + _initializeHighchartsExtensions() { + // enable highcharts accessibility with wrapper function + accessibility(Highcharts); + // patternFill highcharts wrapper function enables rendering patterns instead of just solid colors + patternFill(Highcharts); + // add custom legend wrapper function + ebayLegend(Highcharts); + // add custom columns wrapper to enable rounded bar corners, and stacks with spaces between each stacked point + eBayColumns(Highcharts); + } + _setupCharts() { + // check if a single series was passed in for series and if so add it to a new array + const series = Array.isArray(this.input.series) ? this.input.series : [this.input.series]; + const stacked = this.input.stacked; + const title = this.input.title; + // controls rounded corders and spacing at the bottom of data points + if (stacked) { + series[0].bottom = true; // set a variable on the first series so it renders rounder corners on the bottom of the bar + series[series.length - 1].top = true; // set a variable on the last series to render rounded corner on the top of the bar + + series.forEach((s) => { + // used to help link each series to the previous one for stacked views + // refer to https://api.highcharts.com/highcharts/series.column.linkedTo + s.group = ':previous'; + }); + } else { + // if not stacked, set the top and bottom flag on each series so the single bar has rounded top and bottom corners + series.forEach((s) => { + s.top = true; + s.bottom = true; + }); + } + setSeriesColors(series); + + const config = { + title: { + text: title, // set the title that will render above the chart + }, + chart: this.getChartConfig(), + colors: colorMapping, + xAxis: this.getXAxisConfig(), + yAxis: this.getYAxisConfig(series), + legend: this.getLegendConfig(), + tooltip: this.getTooltipConfig(), + plotOptions: this.getPlotOptionsConfig(), + series, + credits: { + enabled: false, // hide the highcharts label and link in the bottom right + }, + }; + this.chartRef = Highcharts.chart(this.getContainerId(), config); + this.chartRef.redraw(); + } + + getChartConfig() { + return { + type: 'column', + backgroundColor, + style: { + fontFamily: chartFontFamily, // set the font for all chart svg text elements + }, + }; + } + + getXAxisConfig() { + const xAxisLabelFormat = this.input.xAxisLabelFormat; + const xAxisPositioner = this.input.xAxisPositioner; + return { + // currently setup to support epoch time values for xAxisLabels. + // It is possible to set custom non datetime xAxisLabels but will need changes to this component + type: 'datetime', + labels: { + // input.xAxisLabelFormat allows overriding the default short month / day label + // refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat to customize + format: xAxisLabelFormat || '{value:%b %e}', + align: 'center', + style: { + color: labelsColor, // setting label colors + }, + }, + tickWidth: 0, // hide the vertical tick on xAxis labels + tickPositioner: xAxisPositioner, // optional input to allow configuring the position of xAxis tick marks + }; + } + + getYAxisConfig(series) { + const yAxisLabels = this.input.yAxisLabels; + const yAxisPositioner = this.input.yAxisPositioner; + + let maxVal = 0; // use to determine the highest yAxis value + series.forEach((s) => { + maxVal = s.data.reduce((p, c) => (c > p ? c : p), maxVal); + }); + let yLabelsItterator = 0; + return { + gridLineColor: gridColor, // sets the horizontal grid line colors + opposite: true, // moves yAxis labels to the right side of the chart + reversedStacks: false, // makes so series one starts at the bottom of the yAxis, by default this is true + labels: { + format: !yAxisLabels && '${text}', + // if yAxisLabels array is passed in this formatter function is needed to + // return the proper label for each yAxis tick mark + formatter: + yAxisLabels && + function () { + if (this.isFirst) { + yLabelsItterator = -1; + } + yLabelsItterator = yLabelsItterator + 1; + return yAxisLabels[yLabelsItterator]; + }, + style: { + color: labelsColor, // setting label colors + }, + }, + maxVal, + title: { + enabled: false, // hide the axis label next to the axis + }, + offset: 0, // set to zero for no offset refer to https://api.highcharts.com/highcharts/yAxis.offset + // passed in function for yAxisPositioner refer to https://api.highcharts.com/highcharts/yAxis.tickPositioner for use + tickPositioner: yAxisPositioner, + }; + } + + getLegendConfig() { + const series = this.input.series; + return { + symbolRadius: 2, // set corner radius of legend identifiers + enabled: series.length > 1, // disabled legend if only one series is passed in + itemStyle: { + color: legendColor, // set the color of the legend text + }, + itemHiddenStyle: { + color: legendInactiveColor, // set the color of the legend text when clicked to hide + }, + itemHoverStyle: { + color: legendHoverColor, // set the color of the legend text when hovering + }, + }; + } + // returns a function that can be called on each mouseover event + tooltipFormatter() { + const stacked = this.input.stacked; + return function () { + // references to the charts updates series array, only available when the returned tooltip function is called and not before + const series = this.series.chart.series; + + // refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat for dateFormat variables + return subtemplate.renderToString({ + date: Highcharts.dateFormat('%b %e, %Y', this.x, false), + data: stacked ? series : this.point, + stacked, + x: this.x, + }); + }; + } + tooltipPositioner(labelWidth, labelHeight) { + const series = this.chart.series; + const chartPosition = this.chart.pointer.getChartPosition(); // returns the pointers top and left positions + const hpIndex = this.chart.hoverPoint.index; // reference to the index of the original hovered point of the series + const hoverPoint = this.chart.hoverPoint, // reference to the original hovered point of the series + y = // setting the y position of the tooltip to the top of the hovered stack of points + chartPosition.top + + hoverPoint.series.yAxis.top + + series[series.length - 1].data[hpIndex].shapeY - + labelHeight - + 15; // adjust for the arrow + let x = // setting the x position of the tooltip based on the center of the hovered stack of points + chartPosition.left + + hoverPoint.dlBox.x + + hoverPoint.dlBox.width * 0.5 - + labelWidth * 0.5 + + 3; // offset padding + // check left bound and adjust if the tooltip would be clipped + if (x < 6) { + x = 6; + } + // check right bound and adjust if the tooltip would be clipped + if (x + hoverPoint.dlBox.width > chartPosition.left + this.chart.chartWidth - 6) { + x = chartPosition.left + this.chart.chartWidth - hoverPoint.dlBox.width - 6; + } + return { x, y }; // return the tooltip x and y position + } + + getTooltipConfig() { + const stacked = this.input.stacked; + return { + formatter: this.tooltipFormatter(), + useHTML: true, // allows defining html to format tooltip content + backgroundColor: tooltipBackgroundColor, // sets tooltip background color + borderWidth: 0, // hide the default border stroke + borderRadius: 10, // set the border radius of the tooltip + outside: true, // used to render the tooltip outside of the main SVG element + shadow: false, // hide the default shadow as it conflicts with designs + style: { + filter: tooltipShadows, // sets tooltip shadows + fontSize: '12px', + }, + // this callback function is used to position the tooltip at the top of the stacked bars + positioner: stacked && this.tooltipPositioner, + }; + } + + legendItemClick() { + // returns a function so that can access input values + const stacked = this.input.stacked; + return function () { + const series = this.chart.series; + if (stacked) { + // setTimeout with 0 ms to push this function to the end of the execution stack to prevent issues with hover events + setTimeout(() => { + let topFound = false; + let bottomFound = false; + // loop through and reset bottom variables on series based on their visibility + for (let i = 0; i < series.length; i++) { + if (!bottomFound && series[i].visible) { + series[i].options.bottom = true; + bottomFound = true; + } else { + series[i].options.bottom = false; + } + } + + // loop through and reset top variables on series based on their visibility + for (let i = series.length - 1; i >= 0; i--) { + if (!topFound && series[i].visible) { + series[i].options.top = true; + topFound = true; + } else { + series[i].options.top = false; + } + } + this.chart.redraw(); // redraw the chart after all series variables have been updated + }, 0); + } + }; + } + + // handleMouseOver returns a function while keeping scope to the class compnent to access input values + handleMouseOver() { + const stacked = this.input.stacked; + return function () { + const refPoint = this; // this is the active hovered point of the series + const chart = this.series.chart; + chart.series.forEach( + ( + s // loop through each series + ) => + s.points.forEach((p) => { + // loop through each point in the series + if ( + (stacked && p.x !== refPoint.x) || // if the stacked flag is set to true and each points x value does not match + (!stacked && p !== refPoint) // or if not stacked and refPoint does not match the current point + ) { + p.update( + { + opacity: 0.2, // set opacity + }, + false // do not update immediately + ); + } + }) + ); + chart.redraw(); // trigger chart redraw after all points have been updated + }; + } + + handleMouseOut() { + const chart = this.series.chart; + chart.series.forEach( + ( + s // loop through each series + ) => + s.points.forEach( + ( + p // loop through each point in the series + ) => + p.update( + { + opacity: 1, // update the opacity to 1 + }, + false // do not update immediately + ) + ) + ); + chart.redraw(); // trigger chart redraw after all points have been updated + } + + getPlotOptionsConfig() { + const description = this.input.description; + const stacked = this.input.stacked; + return { + series: { + description, + }, + column: { + events: { + legendItemClick: this.legendItemClick(), + }, + stacking: stacked ? 'normal' : null, // set stacking to normal if stacked flag is set + groupPadding: 0.1, // padding around groups of points + pointPadding: 0.15, // padding between single points + states: { + inactive: { + opacity: 1, // prevents other points in the same stack from fading out + }, + }, + point: { + events: { + mouseOver: this.handleMouseOver(), // handleMouseOver returns a function so it can access component input values + mouseOut: this.handleMouseOut, + }, + }, + }, + }; + } + onDestroy() { + this.chartRef.destroy(); + } +} diff --git a/src/components/ebay-bar-chart/examples/data.json b/src/components/ebay-bar-chart/examples/data.json new file mode 100644 index 000000000..509a9f94d --- /dev/null +++ b/src/components/ebay-bar-chart/examples/data.json @@ -0,0 +1,632 @@ +[ + { + "data": [ + { + "x": 1643673600000, + "y": 686.42, + "label": "$686.42" + }, + { + "x": 1643760000000, + "y": 3395.53, + "label": "$3395.53" + }, + { + "x": 1643846400000, + "y": 4623.43, + "label": "$4623.43" + }, + { + "x": 1643932800000, + "y": 742.12, + "label": "$742.12" + }, + { + "x": 1644019200000, + "y": 4525.82, + "label": "$4525.82" + }, + { + "x": 1644105600000, + "y": 1568.92, + "label": "$1568.92" + }, + { + "x": 1644192000000, + "y": 3511.5, + "label": "$3511.5" + }, + { + "x": 1644278400000, + "y": 313.66, + "label": "$313.66" + }, + { + "x": 1644364800000, + "y": 653.18, + "label": "$653.18" + }, + { + "x": 1644451200000, + "y": 3117, + "label": "$3117" + }, + { + "x": 1644537600000, + "y": 4497.92, + "label": "$4497.92" + }, + { + "x": 1644624000000, + "y": 4206.77, + "label": "$4206.77" + }, + { + "x": 1644710400000, + "y": 4640.38, + "label": "$4640.38" + }, + { + "x": 1644796800000, + "y": 1849.15, + "label": "$1849.15" + }, + { + "x": 1644883200000, + "y": 4105.95, + "label": "$4105.95" + }, + { + "x": 1644969600000, + "y": 2996.36, + "label": "$2996.36" + }, + { + "x": 1645056000000, + "y": 2358.2, + "label": "$2358.2" + }, + { + "x": 1645142400000, + "y": 3641.09, + "label": "$3641.09" + }, + { + "x": 1645228800000, + "y": 3654.44, + "label": "$3654.44" + }, + { + "x": 1645315200000, + "y": 1418.79, + "label": "$1418.79" + }, + { + "x": 1645401600000, + "y": 207.46, + "label": "$207.46" + }, + { + "x": 1645488000000, + "y": 1606.93, + "label": "$1606.93" + }, + { + "x": 1645574400000, + "y": 604.25, + "label": "$604.25" + }, + { + "x": 1645660800000, + "y": 3205.45, + "label": "$3205.45" + } + ], + "title": "Bar Chart", + "name": "Value 1" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 2016.88, + "label": "$2016.88" + }, + { + "x": 1643760000000, + "y": 3035.94, + "label": "$3035.94" + }, + { + "x": 1643846400000, + "y": 1452.5, + "label": "$1452.5" + }, + { + "x": 1643932800000, + "y": 582.67, + "label": "$582.67" + }, + { + "x": 1644019200000, + "y": 1283.86, + "label": "$1283.86" + }, + { + "x": 1644105600000, + "y": 3912.73, + "label": "$3912.73" + }, + { + "x": 1644192000000, + "y": 2448.16, + "label": "$2448.16" + }, + { + "x": 1644278400000, + "y": 717.37, + "label": "$717.37" + }, + { + "x": 1644364800000, + "y": 590.24, + "label": "$590.24" + }, + { + "x": 1644451200000, + "y": 1308.96, + "label": "$1308.96" + }, + { + "x": 1644537600000, + "y": 3744.15, + "label": "$3744.15" + }, + { + "x": 1644624000000, + "y": 1693.57, + "label": "$1693.57" + }, + { + "x": 1644710400000, + "y": 4594.56, + "label": "$4594.56" + }, + { + "x": 1644796800000, + "y": 3426.48, + "label": "$3426.48" + }, + { + "x": 1644883200000, + "y": 2364.75, + "label": "$2364.75" + }, + { + "x": 1644969600000, + "y": 3338.47, + "label": "$3338.47" + }, + { + "x": 1645056000000, + "y": 2217.38, + "label": "$2217.38" + }, + { + "x": 1645142400000, + "y": 4269.42, + "label": "$4269.42" + }, + { + "x": 1645228800000, + "y": 1811.27, + "label": "$1811.27" + }, + { + "x": 1645315200000, + "y": 4247.46, + "label": "$4247.46" + }, + { + "x": 1645401600000, + "y": 3230.72, + "label": "$3230.72" + }, + { + "x": 1645488000000, + "y": 236.64, + "label": "$236.64" + }, + { + "x": 1645574400000, + "y": 2960.39, + "label": "$2960.39" + }, + { + "x": 1645660800000, + "y": 720.03, + "label": "$720.03" + } + ], + "title": "Bar Chart", + "name": "Value 2" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 3744.38, + "label": "$3744.38" + }, + { + "x": 1643760000000, + "y": 4091.42, + "label": "$4091.42" + }, + { + "x": 1643846400000, + "y": 1269.12, + "label": "$1269.12" + }, + { + "x": 1643932800000, + "y": 4966.14, + "label": "$4966.14" + }, + { + "x": 1644019200000, + "y": 1268.94, + "label": "$1268.94" + }, + { + "x": 1644105600000, + "y": 4357.47, + "label": "$4357.47" + }, + { + "x": 1644192000000, + "y": 3517.96, + "label": "$3517.96" + }, + { + "x": 1644278400000, + "y": 4678.7, + "label": "$4678.7" + }, + { + "x": 1644364800000, + "y": 4080.55, + "label": "$4080.55" + }, + { + "x": 1644451200000, + "y": 1224.7, + "label": "$1224.7" + }, + { + "x": 1644537600000, + "y": 356.99, + "label": "$356.99" + }, + { + "x": 1644624000000, + "y": 1534.12, + "label": "$1534.12" + }, + { + "x": 1644710400000, + "y": 328.14, + "label": "$328.14" + }, + { + "x": 1644796800000, + "y": 4199.1, + "label": "$4199.1" + }, + { + "x": 1644883200000, + "y": 4078.09, + "label": "$4078.09" + }, + { + "x": 1644969600000, + "y": 1475.89, + "label": "$1475.89" + }, + { + "x": 1645056000000, + "y": 2874.13, + "label": "$2874.13" + }, + { + "x": 1645142400000, + "y": 2888.78, + "label": "$2888.78" + }, + { + "x": 1645228800000, + "y": 658.46, + "label": "$658.46" + }, + { + "x": 1645315200000, + "y": 2050.67, + "label": "$2050.67" + }, + { + "x": 1645401600000, + "y": 1971.17, + "label": "$1971.17" + }, + { + "x": 1645488000000, + "y": 4168.71, + "label": "$4168.71" + }, + { + "x": 1645574400000, + "y": 425.5, + "label": "$425.5" + }, + { + "x": 1645660800000, + "y": 3665.67, + "label": "$3665.67" + } + ], + "title": "Bar Chart", + "name": "Value 3" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 3840.06, + "label": "$3840.06" + }, + { + "x": 1643760000000, + "y": 4828.12, + "label": "$4828.12" + }, + { + "x": 1643846400000, + "y": 2265.45, + "label": "$2265.45" + }, + { + "x": 1643932800000, + "y": 4496.69, + "label": "$4496.69" + }, + { + "x": 1644019200000, + "y": 4393.03, + "label": "$4393.03" + }, + { + "x": 1644105600000, + "y": 624.28, + "label": "$624.28" + }, + { + "x": 1644192000000, + "y": 3722.88, + "label": "$3722.88" + }, + { + "x": 1644278400000, + "y": 4590.79, + "label": "$4590.79" + }, + { + "x": 1644364800000, + "y": 1679.32, + "label": "$1679.32" + }, + { + "x": 1644451200000, + "y": 4748.64, + "label": "$4748.64" + }, + { + "x": 1644537600000, + "y": 1822.92, + "label": "$1822.92" + }, + { + "x": 1644624000000, + "y": 1384.22, + "label": "$1384.22" + }, + { + "x": 1644710400000, + "y": 2966.97, + "label": "$2966.97" + }, + { + "x": 1644796800000, + "y": 4730.39, + "label": "$4730.39" + }, + { + "x": 1644883200000, + "y": 3929.62, + "label": "$3929.62" + }, + { + "x": 1644969600000, + "y": 3731.83, + "label": "$3731.83" + }, + { + "x": 1645056000000, + "y": 4785.14, + "label": "$4785.14" + }, + { + "x": 1645142400000, + "y": 2657.6, + "label": "$2657.6" + }, + { + "x": 1645228800000, + "y": 4816.07, + "label": "$4816.07" + }, + { + "x": 1645315200000, + "y": 4493.61, + "label": "$4493.61" + }, + { + "x": 1645401600000, + "y": 2887.25, + "label": "$2887.25" + }, + { + "x": 1645488000000, + "y": 1905.9, + "label": "$1905.9" + }, + { + "x": 1645574400000, + "y": 706.83, + "label": "$706.83" + }, + { + "x": 1645660800000, + "y": 1516, + "label": "$1516" + } + ], + "title": "Bar Chart", + "name": "Value 4" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 3510.98, + "label": "$3510.98" + }, + { + "x": 1643760000000, + "y": 2558.58, + "label": "$2558.58" + }, + { + "x": 1643846400000, + "y": 2028.93, + "label": "$2028.93" + }, + { + "x": 1643932800000, + "y": 3998.57, + "label": "$3998.57" + }, + { + "x": 1644019200000, + "y": 1234.8, + "label": "$1234.8" + }, + { + "x": 1644105600000, + "y": 3512.28, + "label": "$3512.28" + }, + { + "x": 1644192000000, + "y": 554.04, + "label": "$554.04" + }, + { + "x": 1644278400000, + "y": 4934.67, + "label": "$4934.67" + }, + { + "x": 1644364800000, + "y": 3190.14, + "label": "$3190.14" + }, + { + "x": 1644451200000, + "y": 2894.57, + "label": "$2894.57" + }, + { + "x": 1644537600000, + "y": 68.99, + "label": "$68.99" + }, + { + "x": 1644624000000, + "y": 3051.49, + "label": "$3051.49" + }, + { + "x": 1644710400000, + "y": 1766.32, + "label": "$1766.32" + }, + { + "x": 1644796800000, + "y": 1998.19, + "label": "$1998.19" + }, + { + "x": 1644883200000, + "y": 3695.29, + "label": "$3695.29" + }, + { + "x": 1644969600000, + "y": 46.56, + "label": "$46.56" + }, + { + "x": 1645056000000, + "y": 3877.75, + "label": "$3877.75" + }, + { + "x": 1645142400000, + "y": 2752.76, + "label": "$2752.76" + }, + { + "x": 1645228800000, + "y": 4341.6, + "label": "$4341.6" + }, + { + "x": 1645315200000, + "y": 1836.76, + "label": "$1836.76" + }, + { + "x": 1645401600000, + "y": 346.63, + "label": "$346.63" + }, + { + "x": 1645488000000, + "y": 1149.93, + "label": "$1149.93" + }, + { + "x": 1645574400000, + "y": 4566.77, + "label": "$4566.77" + }, + { + "x": 1645660800000, + "y": 4337.08, + "label": "$4337.08" + } + ], + "title": "Bar Chart", + "name": "Value 5" + } +] diff --git a/src/components/ebay-bar-chart/index.marko b/src/components/ebay-bar-chart/index.marko new file mode 100644 index 000000000..df5a2ca06 --- /dev/null +++ b/src/components/ebay-bar-chart/index.marko @@ -0,0 +1,20 @@ +import { processHtmlAttributes } from "../../common/html-attributes" +static var ignoredAttributes = [ + 'class', + 'series', + 'title', + 'description', + 'xAxisLabelFormat', + 'yAxisLabels', + 'xAxisPositioner', + 'yAxisPositioner' +]; + +
+ +
diff --git a/src/components/ebay-bar-chart/marko-tag.json b/src/components/ebay-bar-chart/marko-tag.json new file mode 100644 index 000000000..9c934e86a --- /dev/null +++ b/src/components/ebay-bar-chart/marko-tag.json @@ -0,0 +1,23 @@ +{ + "attribute-groups": ["html-attributes"], + "@*": { + "targetProperty": null, + "type": "expression" + }, + "@html-attributes": "expression", + "@title": "string", + "@description": "string", + "@series[]": { + "@title": "string", + "@data[]": { + "@x": "number", + "@y": "number", + "@label": "string" + } + }, + "@xAxisLabelFormat": "string", + "@xAxisPositioner": "function", + "@yAxisLabels": "array", + "@yAxisPositioner": "function", + "@stacked": "boolean" +} diff --git a/src/components/ebay-bar-chart/style.less b/src/components/ebay-bar-chart/style.less new file mode 100644 index 000000000..1ef2b9df4 --- /dev/null +++ b/src/components/ebay-bar-chart/style.less @@ -0,0 +1,15 @@ +.ebay-bar-chart { + width: 100%; +} + +.ebay-bar-chart .highcharts-legend-item-hidden { + transition: fill 250ms; +} + +.ebay-bar-chart .highcharts-legend-item-hidden .highcharts-point { + stroke-width: 0; +} + +.highcharts-tooltip-container svg { + overflow: visible; +} diff --git a/src/components/ebay-bar-chart/subtemplate.marko b/src/components/ebay-bar-chart/subtemplate.marko new file mode 100644 index 000000000..8b6059249 --- /dev/null +++ b/src/components/ebay-bar-chart/subtemplate.marko @@ -0,0 +1,16 @@ +${input.date} + +
${input.data.label}
+ + + + + +
+ ${series.name} + ${point.label} +
+ + + +
diff --git a/src/components/ebay-line-chart/README.md b/src/components/ebay-line-chart/README.md new file mode 100644 index 000000000..f0e53fe12 --- /dev/null +++ b/src/components/ebay-line-chart/README.md @@ -0,0 +1,10 @@ +

+ + ebay-line-chart + + + DS v3.7.0 + +

+ +The line chart displays one to five series of data points as an interactive chart diff --git a/src/components/ebay-line-chart/component.js b/src/components/ebay-line-chart/component.js new file mode 100644 index 000000000..6dd9df6e3 --- /dev/null +++ b/src/components/ebay-line-chart/component.js @@ -0,0 +1,424 @@ +import Highcharts from 'highcharts'; +import accessibility from 'highcharts/modules/accessibility'; +import { + chartFontFamily, + backgroundColor, + gridColor, + labelsColor, + legendColor, + legendInactiveColor, + legendHoverColor, + tooltipBackgroundColor, + tooltipShadows, + lineChartPrimaryColor, + lineChartSecondaryColor, + lineChartTertiaryColor, + lineChartQueternaryColor, + lineChartQuinaryColor, + trendPositiveColor, + trendNegativeColor, +} from '../../common/charts/shared'; +import { debounce } from '../../common/event-utils'; +import tooltipTemplate from './tooltip.marko'; + +const pointSize = 6; // controls the size of the plot point markers on lines + +export default class { + onCreate() { + this.axisTicksLength = -1; + } + onMount() { + this._initializeHighchartsExtensions(); + this._setupChart(); + } + onInput() { + // if chartRef does not exist do not try to run setupCharts as it may be server side and highcharts only works on the client side + if (this.chartRef && this.chartRef.destroy) { + this.chartRef.destroy(); + this._setupCharts(); + } + } + getContainerId() { + return `ebay-line-graph-${this.id}`; + } + _initializeHighchartsExtensions() { + // enable highcharts accessibility with wrapper function + accessibility(Highcharts); + } + _setupChart() { + const colors = [ + // configure the array of colors to use for each series + lineChartPrimaryColor, + lineChartSecondaryColor, + lineChartTertiaryColor, + lineChartQueternaryColor, + lineChartQuinaryColor, + ]; + + // check if a single series was passed in for series and if so add it to a new array + const series = Array.isArray(this.input.series) ? this.input.series : [this.input.series]; + + if (this.input.trend) { + // if the trend property exist check value and adjust the first color + const trend = typeof this.input.trend === 'string' && this.input.trend.toLowerCase(); // if trend of type string force to lowercase + const isPositive = series[0].data[0].y < series[0].data[series[0].data.length - 1].y; // auto trend positive check between first and last data values of the single series + if ( + trend === 'positive' || // if "positive" is passed in for the trend property + (trend !== 'negative' && trend !== 'neutral' && isPositive) // if check if trend is does not equal negative or neutral, and if so use the auto positive calculation + ) { + colors[0] = trendPositiveColor; // set the color to the positive trend color + } else if (trend === 'negative' || (trend !== 'neutral' && !isPositive)) { + // if the trend property equals negative, or trend does not equal neutral and isPositive is false + colors[0] = trendNegativeColor; // set the negative trend color + } + } + + // configure the symbol used for each series markers + series.forEach((s, i) => { + s.marker = { + symbol: this.getSymbol(i), + }; + }); + + const chart = this.getChartConfig(); + const xAxis = this.getXAxisConfig(); + const yAxis = this.getYAxisConfig(series); + const legend = this.getLegendConfig(); + const tooltip = this.getTooltipConfig(); + const plotOptions = this.getPlotOptionsConfig(series); + + const title = { + text: this.input.title, + align: 'left', + useHTML: true, + style: { + fontSize: '18px', + fontWeight: 700, + }, + }; + + const config = { + title, + chart, + colors, + xAxis, + yAxis, + legend, + tooltip, + plotOptions, + series, // pass in the configured series array + credits: { + enabled: false, // hide the highcharts label and link in the bottom right of chart + }, + }; + + // initialize and keep reference to chart + this.chartRef = Highcharts.chart(this.getContainerId(), config); + // call update markers after the initial render to determine which markers to display if plotPoints is set to true + this.updateMarkers(); + } + + getSymbol(index) { + let s; + switch (index) { + case 1: + s = 'square'; + break; + case 2: + s = 'triangle'; + break; + case 3: + s = 'triangle-down'; + break; + case 4: + s = 'diamond'; + break; + default: // 0 index + s = 'circle'; + break; + } + return s; + } + getChartConfig() { + return { + type: 'line', + backgroundColor: backgroundColor, + style: { + fontFamily: chartFontFamily, + }, + events: { + // on chart redraw trigger updateMarkers to look for changes in xAxis tick marks and adjust series markers visibility accordingly + redraw: () => this.updateMarkers(), + }, + }; + } + getXAxisConfig() { + return { + // currently setup to support epoch time values for xAxisLabels. + // It is possible to set custom non datetime xAxisLabels but will need changes to this component + type: 'datetime', + labels: { + // input.xAxisLabelFormat allows overriding the default short month / day label + // refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat to customize + format: this.input.xAxisLabelFormat || '{value:%b %e}', + align: 'center', + style: { + color: labelsColor, // setting label colors + }, + }, + tickWidth: 0, // hide the vertical tick on xAxis labels + tickPositioner: this.input.xAxisPositioner, // optional input to allow configuring the position of xAxis tick marks + }; + } + getYAxisConfig(series) { + const component = this; // component reference used in formatter functions that don't have the same scope + let yLabelsItterator = 0; // used when yAxisLabels array is provided in input + let maxVal = 0; // use to determine the highest yAxis value + // configure the symbol used for each series markers + series.forEach((seriesItem) => { + maxVal = Math.max(...seriesItem.data, maxVal); + }); + return { + gridLineColor: gridColor, // sets the horizontal grid line colors + opposite: true, // moves yAxis labels to the right side of the chart + labels: { + // if yAxisLabels are not passed in display the standard label + format: !this.input.yAxisLabels && '${text}', + // if yAxisLabels array is passed in this formatter function is needed to + // return the proper label for each yAxis tick mark + formatter: + this.input.yAxisLabels && + function () { + if (this.isFirst) { + yLabelsItterator = -1; + } + yLabelsItterator = yLabelsItterator + 1; + return component.input.yAxisLabels[yLabelsItterator]; + }, + style: { + color: labelsColor, // setting label colors + }, + }, + maxVal, + title: { + enabled: false, // hide the axis label next to the axis + }, + offset: 0, // set to zero for no offset refer to https://api.highcharts.com/highcharts/yAxis.offset + // passed in function for yAxisPositioner refer to https://api.highcharts.com/highcharts/yAxis.tickPositioner for use + tickPositioner: this.input.yAxisPositioner, + }; + } + getLegendConfig() { + return { + // if only a single series is provided do not display the legend + enabled: this.input.series.length > 1, + symbolRadius: 6, // corner radius on legend identifiers svg element + symbolWidth: 12, // setting the width of the legend identifiers svg element + symbolHeight: 12, // setting the height of the legend identifiers svg element + itemStyle: { + color: legendColor, // set the color of the text in the legend + }, + itemHiddenStyle: { + color: legendInactiveColor, // set legend text color when legend item has been clicked and hidden + }, + itemHoverStyle: { + color: legendHoverColor, // set legend text color on hover of legend element + }, + }; + } + getTooltipConfig() { + const component = this; // component reference used in formatter functions that don't have the same scope + return { + formatter: function () { + // refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat for dateFormat variables + return tooltipTemplate.renderToString({ + date: Highcharts.dateFormat('%b %e, %Y', this.points[0].x, false), + points: this.points, + seriesLength: component.input.series.length > 1, + }); + }, + useHTML: true, // allows defining html to format tooltip content + backgroundColor: tooltipBackgroundColor, // sets tooltip background color + borderWidth: 0, // hide the default border stroke + borderRadius: 10, // set the border radius of the tooltip + outside: true, // used to render the tooltip outside of the main SVG element + shadow: false, // hide the default shadow as it conflicts with designs + crosshairs: { + dashStyle: 'solid', // makes a yaxis cross hair appear over the hovered xAxis data points + }, + shared: true, // shared means that if there are multipe series passed in there will be a single tooltip element per xAxis point + style: { + filter: tooltipShadows, // sets tooltip shadows + fontSize: '12px', + }, + }; + } + getPlotOptionsConfig(series) { + const mouseOut = debounce(() => this.handleMouseOut(), 80); + const mouseOver = debounce((e) => this.handleMouseOver(e), 85); // 85ms delay for debounce so it doesn't colide with mouseOut debounce calls + + return { + line: { + events: { + // assign mouse events to point hovers + mouseOut, + }, + }, + series: { + description: this.input.description, // set the description that was passed in + lineWidth: 3, // sets the line width for series lines + // sets the starting point of the xAxis to the first data point + // if not set the auto resizing of the xAxis will often leave a gap in data on the left hand side + pointStart: series[0].data[0].x, + point: { + // assign mouse events to point hovers + events: { + mouseOver, + mouseOut, + }, + }, + }, + }; + } + handleMouseOut() { + // this function is debounced to improve performance + this.chartRef.series.forEach((s) => { + s.data.forEach((data) => { + // check if hover is on the xAxis (onTick) for each item, + // and if they have a className remove and disable the marker + if (!data.onTick && data.className !== null) { + data.update( + { + className: null, // nullify className if not active + marker: { + enabled: false, // disable marker if not active + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } else if (data.onTick && data.className === null) { + data.update( + { + className: 'ebay-line-graph__marker--visible', // set classname + onTick: data.onTick, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: true, // set marker enabled + radius: pointSize, // set the size of marker + lineColor: backgroundColor, // set border color of hover markers + lineWidth: 2, // sets the border line width of the marker symbol + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } + }); + }); + this.chartRef.redraw(); // trigger redraw after all points have been updated + } + handleMouseOver(e) { + // this function is debounced to improve performance + this.chartRef.series.forEach((s) => { + s.data.forEach((data) => { + // if active xAxis hover position matches the data point x update the marker to display + if (data.x === e.target.x) { + data.update( + { + className: 'ebay-line-graph__marker--visible', // sets the classname + onTick: data.onTick, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: true, // set marker enabled + radius: pointSize, // set the size of marker + lineColor: backgroundColor, // set border color of hover markers + lineWidth: 2, // sets the border line width of the marker symbol + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } else if (!data.onTick && data.className !== null) { + data.update( + { + className: null, // nullify className if not active + onTick: data.onTick, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: false, // disable marker + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } + }); + }); + this.chartRef.redraw(); // trigger redraw after all points have been updated + } + updateMarkers(e) { + if (this.input.plotPoints) { + // ticks is an object with the xaxis date values as their keys + // setting tickValues to the keys of the ticks object and parsing into an int for data matching of xValues in series below + this.tickValues = Object.keys(this.chartRef.axes[0].ticks).map((value) => + parseInt(value, 10) + ); + + // this checks if the resize has adjust the number of xAxis tick marks, and if so make updates + if (this.axisTicksLength !== this.tickValues.length || e === true) { + // update the axisTicksLenth variable used for checks in the updateMarkers calls + this.axisTicksLength = this.tickValues.length; + // loops through each series if a className exist remove and hide the marker + this.chartRef.series.forEach((series) => { + // looping through each series data array + series.data.forEach((data) => { + if (data.className !== null) { + data.update( + { + className: null, // removing className used to help keep track of active markers + onTick: false, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: false, // disable the marker + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } + }); + }); + + // loop through each series again and update markers that line up to xAxis tick marks + this.chartRef.series.forEach((series) => { + // loop through each searies data objects + series.data.forEach((data) => { + // loop through the tickValues that come from the x axis ticks and are epoch time stamps + this.tickValues.forEach((tick) => { + // if the current point x value matches the tickValue or the updateMarkers event exist from the redraw event + if (tick === data.x || data.x === e) { + if (data.className === null) { + data.update( + { + className: 'ebay-line-graph__marker--visible', // add the ebay-line-graph__marker--visible class to boost it's visibility + onTick: true, // sets the onTick flag to keep track of the points enabled status for mouse events + marker: { + enabled: true, // set marker enabled + radius: pointSize, // set the size of the marker + lineColor: backgroundColor, // set the border color of the hover markers + lineWidth: 2, // set the border width of the hover markers + }, + }, + false, // disable auto redraw + false // disable auto animation + ); + } + } + }); + }); + }); + this.chartRef.redraw(); // trigger redraw after all points have been updated + } + } + } + onDestroy() { + this.chartRef.destroy(); + } +} diff --git a/src/components/ebay-line-chart/examples/data.json b/src/components/ebay-line-chart/examples/data.json new file mode 100644 index 000000000..cc8aa9c9e --- /dev/null +++ b/src/components/ebay-line-chart/examples/data.json @@ -0,0 +1,757 @@ +[ + { + "data": [ + { + "x": 1643673600000, + "y": -811.28, + "label": "$-811.28" + }, + { + "x": 1643760000000, + "y": -1804.83, + "label": "$-1804.83" + }, + { + "x": 1643846400000, + "y": -885.85, + "label": "$-885.85" + }, + { + "x": 1643932800000, + "y": 949.67, + "label": "$949.67" + }, + { + "x": 1644019200000, + "y": 21.24, + "label": "$21.24" + }, + { + "x": 1644105600000, + "y": 623.81, + "label": "$623.81" + }, + { + "x": 1644192000000, + "y": -1386.42, + "label": "$-1386.42" + }, + { + "x": 1644278400000, + "y": -2963.25, + "label": "$-2963.25" + }, + { + "x": 1644364800000, + "y": -3829.14, + "label": "$-3829.14" + }, + { + "x": 1644451200000, + "y": -2878.11, + "label": "$-2878.11" + }, + { + "x": 1644537600000, + "y": -2999.07, + "label": "$-2999.07" + }, + { + "x": 1644624000000, + "y": -1639.63, + "label": "$-1639.63" + }, + { + "x": 1644710400000, + "y": 607.03, + "label": "$607.03" + }, + { + "x": 1644796800000, + "y": -62.25, + "label": "$-62.25" + }, + { + "x": 1644883200000, + "y": 1808.09, + "label": "$1808.09" + }, + { + "x": 1644969600000, + "y": 3884.39, + "label": "$3884.39" + }, + { + "x": 1645056000000, + "y": 2002.01, + "label": "$2002.01" + }, + { + "x": 1645142400000, + "y": 1186.69, + "label": "$1186.69" + }, + { + "x": 1645228800000, + "y": 1312.35, + "label": "$1312.35" + }, + { + "x": 1645315200000, + "y": 3353.77, + "label": "$3353.77" + }, + { + "x": 1645401600000, + "y": 4770.2, + "label": "$4770.2" + }, + { + "x": 1645488000000, + "y": 2861.46, + "label": "$2861.46" + }, + { + "x": 1645574400000, + "y": 1659, + "label": "$1659" + }, + { + "x": 1645660800000, + "y": -108.29, + "label": "$-108.29" + }, + { + "x": 1645747200000, + "y": -982.37, + "label": "$-982.37" + }, + { + "x": 1645833600000, + "y": 333.09, + "label": "$333.09" + }, + { + "x": 1645920000000, + "y": 2155.24, + "label": "$2155.24" + }, + { + "x": 1646006400000, + "y": 1162.43, + "label": "$1162.43" + }, + { + "x": 1646092800000, + "y": 1166.61, + "label": "$1166.61" + } + ], + "title": "Line Graph", + "name": "Value 1" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 340.3, + "label": "$340.3" + }, + { + "x": 1643760000000, + "y": 2471.82, + "label": "$2471.82" + }, + { + "x": 1643846400000, + "y": 529.56, + "label": "$529.56" + }, + { + "x": 1643932800000, + "y": 1663.67, + "label": "$1663.67" + }, + { + "x": 1644019200000, + "y": 2134.97, + "label": "$2134.97" + }, + { + "x": 1644105600000, + "y": 3761.53, + "label": "$3761.53" + }, + { + "x": 1644192000000, + "y": 1301.55, + "label": "$1301.55" + }, + { + "x": 1644278400000, + "y": 3423.35, + "label": "$3423.35" + }, + { + "x": 1644364800000, + "y": 2747.74, + "label": "$2747.74" + }, + { + "x": 1644451200000, + "y": 5073.93, + "label": "$5073.93" + }, + { + "x": 1644537600000, + "y": 6844.62, + "label": "$6844.62" + }, + { + "x": 1644624000000, + "y": 8460.86, + "label": "$8460.86" + }, + { + "x": 1644710400000, + "y": 9496.45, + "label": "$9496.45" + }, + { + "x": 1644796800000, + "y": 7215.15, + "label": "$7215.15" + }, + { + "x": 1644883200000, + "y": 6527.58, + "label": "$6527.58" + }, + { + "x": 1644969600000, + "y": 7611.97, + "label": "$7611.97" + }, + { + "x": 1645056000000, + "y": 8970.63, + "label": "$8970.63" + }, + { + "x": 1645142400000, + "y": 7097.47, + "label": "$7097.47" + }, + { + "x": 1645228800000, + "y": 9201.15, + "label": "$9201.15" + }, + { + "x": 1645315200000, + "y": 8417.03, + "label": "$8417.03" + }, + { + "x": 1645401600000, + "y": 8853.57, + "label": "$8853.57" + }, + { + "x": 1645488000000, + "y": 9524.27, + "label": "$9524.27" + }, + { + "x": 1645574400000, + "y": 11695.93, + "label": "$11695.93" + }, + { + "x": 1645660800000, + "y": 12302.77, + "label": "$12302.77" + }, + { + "x": 1645747200000, + "y": 13624.34, + "label": "$13624.34" + }, + { + "x": 1645833600000, + "y": 13236.8, + "label": "$13236.8" + }, + { + "x": 1645920000000, + "y": 13940.84, + "label": "$13940.84" + }, + { + "x": 1646006400000, + "y": 16020.66, + "label": "$16020.66" + }, + { + "x": 1646092800000, + "y": 17547.55, + "label": "$17547.55" + } + ], + "title": "Line Graph", + "name": "Value 2" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 2402.49, + "label": "$2402.49" + }, + { + "x": 1643760000000, + "y": 182.51, + "label": "$182.51" + }, + { + "x": 1643846400000, + "y": -2202.33, + "label": "$-2202.33" + }, + { + "x": 1643932800000, + "y": -2588.47, + "label": "$-2588.47" + }, + { + "x": 1644019200000, + "y": -743.1, + "label": "$-743.1" + }, + { + "x": 1644105600000, + "y": -2348.05, + "label": "$-2348.05" + }, + { + "x": 1644192000000, + "y": -3619.6, + "label": "$-3619.6" + }, + { + "x": 1644278400000, + "y": -2658.36, + "label": "$-2658.36" + }, + { + "x": 1644364800000, + "y": -2074.69, + "label": "$-2074.69" + }, + { + "x": 1644451200000, + "y": -4296.3, + "label": "$-4296.3" + }, + { + "x": 1644537600000, + "y": -2029.44, + "label": "$-2029.44" + }, + { + "x": 1644624000000, + "y": -2910.52, + "label": "$-2910.52" + }, + { + "x": 1644710400000, + "y": -3751.44, + "label": "$-3751.44" + }, + { + "x": 1644796800000, + "y": -4322.59, + "label": "$-4322.59" + }, + { + "x": 1644883200000, + "y": -6187.51, + "label": "$-6187.51" + }, + { + "x": 1644969600000, + "y": -3965.52, + "label": "$-3965.52" + }, + { + "x": 1645056000000, + "y": -5565.79, + "label": "$-5565.79" + }, + { + "x": 1645142400000, + "y": -3156.21, + "label": "$-3156.21" + }, + { + "x": 1645228800000, + "y": -741.71, + "label": "$-741.71" + }, + { + "x": 1645315200000, + "y": 131.72, + "label": "$131.72" + }, + { + "x": 1645401600000, + "y": 2329.41, + "label": "$2329.41" + }, + { + "x": 1645488000000, + "y": 743.32, + "label": "$743.32" + }, + { + "x": 1645574400000, + "y": -1386.06, + "label": "$-1386.06" + }, + { + "x": 1645660800000, + "y": -23.59, + "label": "$-23.59" + }, + { + "x": 1645747200000, + "y": -446.53, + "label": "$-446.53" + }, + { + "x": 1645833600000, + "y": -2482.72, + "label": "$-2482.72" + }, + { + "x": 1645920000000, + "y": -1214.99, + "label": "$-1214.99" + }, + { + "x": 1646006400000, + "y": 745.71, + "label": "$745.71" + }, + { + "x": 1646092800000, + "y": 532.04, + "label": "$532.04" + } + ], + "title": "Line Graph", + "name": "Value 3" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 1659.13, + "label": "$1659.13" + }, + { + "x": 1643760000000, + "y": 2010.16, + "label": "$2010.16" + }, + { + "x": 1643846400000, + "y": 2506.64, + "label": "$2506.64" + }, + { + "x": 1643932800000, + "y": 3025.7, + "label": "$3025.7" + }, + { + "x": 1644019200000, + "y": 3961.44, + "label": "$3961.44" + }, + { + "x": 1644105600000, + "y": 3061.87, + "label": "$3061.87" + }, + { + "x": 1644192000000, + "y": 5453.7, + "label": "$5453.7" + }, + { + "x": 1644278400000, + "y": 7225.72, + "label": "$7225.72" + }, + { + "x": 1644364800000, + "y": 5288.24, + "label": "$5288.24" + }, + { + "x": 1644451200000, + "y": 4039.02, + "label": "$4039.02" + }, + { + "x": 1644537600000, + "y": 1949.47, + "label": "$1949.47" + }, + { + "x": 1644624000000, + "y": -131.99, + "label": "$-131.99" + }, + { + "x": 1644710400000, + "y": -190.02, + "label": "$-190.02" + }, + { + "x": 1644796800000, + "y": -2423.83, + "label": "$-2423.83" + }, + { + "x": 1644883200000, + "y": -4627.52, + "label": "$-4627.52" + }, + { + "x": 1644969600000, + "y": -3025.02, + "label": "$-3025.02" + }, + { + "x": 1645056000000, + "y": -1952.73, + "label": "$-1952.73" + }, + { + "x": 1645142400000, + "y": 177.11, + "label": "$177.11" + }, + { + "x": 1645228800000, + "y": -800.98, + "label": "$-800.98" + }, + { + "x": 1645315200000, + "y": 768.99, + "label": "$768.99" + }, + { + "x": 1645401600000, + "y": 655.82, + "label": "$655.82" + }, + { + "x": 1645488000000, + "y": 2238.12, + "label": "$2238.12" + }, + { + "x": 1645574400000, + "y": -38.5, + "label": "$-38.5" + }, + { + "x": 1645660800000, + "y": -1196.35, + "label": "$-1196.35" + }, + { + "x": 1645747200000, + "y": 946.44, + "label": "$946.44" + }, + { + "x": 1645833600000, + "y": 817.48, + "label": "$817.48" + }, + { + "x": 1645920000000, + "y": 293.82, + "label": "$293.82" + }, + { + "x": 1646006400000, + "y": -1531.37, + "label": "$-1531.37" + }, + { + "x": 1646092800000, + "y": 545.94, + "label": "$545.94" + } + ], + "title": "Line Graph", + "name": "Value 4" + }, + { + "data": [ + { + "x": 1643673600000, + "y": 712.41, + "label": "$712.41" + }, + { + "x": 1643760000000, + "y": -1413.36, + "label": "$-1413.36" + }, + { + "x": 1643846400000, + "y": -2274.74, + "label": "$-2274.74" + }, + { + "x": 1643932800000, + "y": -3396.13, + "label": "$-3396.13" + }, + { + "x": 1644019200000, + "y": -2462.16, + "label": "$-2462.16" + }, + { + "x": 1644105600000, + "y": -4740.43, + "label": "$-4740.43" + }, + { + "x": 1644192000000, + "y": -4164.97, + "label": "$-4164.97" + }, + { + "x": 1644278400000, + "y": -1933.18, + "label": "$-1933.18" + }, + { + "x": 1644364800000, + "y": -1938.8, + "label": "$-1938.8" + }, + { + "x": 1644451200000, + "y": -2565.66, + "label": "$-2565.66" + }, + { + "x": 1644537600000, + "y": -3636.67, + "label": "$-3636.67" + }, + { + "x": 1644624000000, + "y": -2633.09, + "label": "$-2633.09" + }, + { + "x": 1644710400000, + "y": -803.22, + "label": "$-803.22" + }, + { + "x": 1644796800000, + "y": 400.72, + "label": "$400.72" + }, + { + "x": 1644883200000, + "y": -1859.62, + "label": "$-1859.62" + }, + { + "x": 1644969600000, + "y": -2777.84, + "label": "$-2777.84" + }, + { + "x": 1645056000000, + "y": -2515.71, + "label": "$-2515.71" + }, + { + "x": 1645142400000, + "y": -3979.22, + "label": "$-3979.22" + }, + { + "x": 1645228800000, + "y": -5790.6, + "label": "$-5790.6" + }, + { + "x": 1645315200000, + "y": -8049.3, + "label": "$-8049.3" + }, + { + "x": 1645401600000, + "y": -7924.14, + "label": "$-7924.14" + }, + { + "x": 1645488000000, + "y": -10198.63, + "label": "$-10198.63" + }, + { + "x": 1645574400000, + "y": -8511.37, + "label": "$-8511.37" + }, + { + "x": 1645660800000, + "y": -6037.33, + "label": "$-6037.33" + }, + { + "x": 1645747200000, + "y": -8014.84, + "label": "$-8014.84" + }, + { + "x": 1645833600000, + "y": -5565.16, + "label": "$-5565.16" + }, + { + "x": 1645920000000, + "y": -4305.32, + "label": "$-4305.32" + }, + { + "x": 1646006400000, + "y": -6573.13, + "label": "$-6573.13" + }, + { + "x": 1646092800000, + "y": -7835.57, + "label": "$-7835.57" + } + ], + "title": "Line Graph", + "name": "Value 5" + } +] diff --git a/src/components/ebay-line-chart/index.marko b/src/components/ebay-line-chart/index.marko new file mode 100644 index 000000000..064857c50 --- /dev/null +++ b/src/components/ebay-line-chart/index.marko @@ -0,0 +1,21 @@ +import { processHtmlAttributes } from "../../common/html-attributes" +static var ignoredAttributes = [ + 'class', + 'series', + 'title', + 'description', + 'xAxisLabelFormat', + 'yAxisLabels', + 'xAxisPositioner', + 'yAxisPositioner', + 'trend' +]; + +
+ +
diff --git a/src/components/ebay-line-chart/line-chart.stories.js b/src/components/ebay-line-chart/line-chart.stories.js new file mode 100644 index 000000000..87420ef57 --- /dev/null +++ b/src/components/ebay-line-chart/line-chart.stories.js @@ -0,0 +1,291 @@ +import { tagToString } from '../../../.storybook/storybook-code-source'; +import { addRenderBodies } from '../../../.storybook/utils'; +import Readme from './README.md'; +import Component from './index.marko'; +import * as sampleSeriesData from './examples/data.json'; + +const Template = (args) => ({ + input: addRenderBodies(args), +}); + +export default { + title: 'charts/ebay-line-chart', + component: Component, + parameters: { + docs: { + description: { + component: Readme, + }, + }, + }, + argTypes: { + title: { + type: { name: 'string', required: false }, + description: 'A title displayed above the graph', + }, + description: { + type: { name: 'string', required: true }, + description: 'A description of what the chart is displaying', + }, + trend: { + type: { name: 'boolean', required: false }, + description: + 'Trend can bet set to `true`, `positive`, or `negative` if set to true the trend is auto calculated by comparing the first and last data points y value', + }, + series: { + type: { name: 'object', required: true }, + description: + 'The series is an array of one to five arrays of point objects, each point contains an `x`, `y`, and `label`. `x` is an epoch/unix time code, `y` is a numeric value, `label` is what is displayed for the `y` value in the tool tip', + }, + plotPoints: { + type: { name: 'boolean', required: false }, + description: + 'Defaults to false, if set to true markers will appear on series lines at the x-axis tick mark positions.', + table: { + defaultValue: { + summary: 'false', + }, + }, + }, + xAxisLabelFormat: { + type: { name: 'string', required: false }, + description: + 'Used to modify the display of the x-axis labels. Accepts a string like `{value:%Y-%m-%d}`. Refer to https://api.highcharts.com/class-reference/Highcharts.Time#dateFormat for available format keys', + table: { + defaultValue: { + summary: '{value:%b %e}', + }, + }, + }, + xAxisPositioner: { + type: { name: 'function', required: false }, + description: + 'A custom function that returns an array of epoch/unix time values where x-axis labels will be displayed. You can access `this.dataMin` and `this.dataMax` from the function to help determine positions.', + }, + yAxisLabels: { + type: { name: 'array', required: false }, + description: + 'An array of labels to use on the y-axis. Use in conjunction with yAxisPositioner. Make sure the length of the yAxisLabels match the length of the positions array returned by the yAxisPositioner function', + }, + yAxisPositioner: { + type: { name: 'function', required: false }, + description: + 'A custom function that returns an array of numeric values where y-axis labels will be displayed. You can access `this.dataMin` and `this.dataMax` from the function to help determine positions', + }, + class: { + type: { name: 'string', require: false }, + description: 'A class name that will be added to the main chart container', + }, + }, +}; + +export const Standard = Template.bind({}); +Standard.args = { + series: sampleSeriesData.slice(0, 1), + title: 'standard line graph with single series', + description: 'sample line graph description', +}; + +Standard.parameters = { + docs: { + source: { + code: tagToString('line-chart', Standard.args), + }, + }, +}; + +export const TrendAutoPositive = Template.bind({}); +TrendAutoPositive.args = { + series: { + data: sampleSeriesData[0].data, + }, + trend: true, + title: 'positive trend graph', + description: 'standard line graph with single series the tend set to true', +}; + +TrendAutoPositive.parameters = { + docs: { + source: { + code: tagToString('line-chart', TrendAutoPositive.args), + }, + }, +}; + +export const TrendSetPositive = Template.bind({}); +TrendSetPositive.args = { + series: { + data: sampleSeriesData[3].data, + }, + trend: 'positive', + title: 'forced positive trend graph', + description: 'standard line graph with single series the tend set to "positive"', +}; + +TrendSetPositive.parameters = { + docs: { + source: { + code: tagToString('line-chart', TrendSetPositive.args), + }, + }, +}; + +export const TrendAutoNegative = Template.bind({}); +TrendAutoNegative.args = { + series: { + data: sampleSeriesData[3].data, + }, + trend: true, + title: 'negative trend graph', + description: 'standard line graph with single series the tend set to true', +}; + +TrendAutoNegative.parameters = { + docs: { + source: { + code: tagToString('line-chart', TrendAutoNegative.args), + }, + }, +}; + +export const TrendSetNegative = Template.bind({}); +TrendSetNegative.args = { + series: { + data: sampleSeriesData[1].data, + }, + trend: 'negative', + title: 'forced negative trend graph', + description: 'standard line graph with single series the tend set to "negative"', +}; + +TrendSetNegative.parameters = { + docs: { + source: { + code: tagToString('line-chart', TrendSetNegative.args), + }, + }, +}; + +export const TrendSetNeutral = Template.bind({}); +TrendSetNeutral.args = { + series: { + data: sampleSeriesData[0].data, + }, + trend: 'neutral', +}; + +TrendSetNeutral.parameters = { + docs: { + source: { + code: tagToString('line-chart', TrendSetNeutral.args), + }, + }, +}; + +export const TwoSeries = Template.bind({}); +TwoSeries.args = { + series: sampleSeriesData.slice(0, 2), +}; + +TwoSeries.parameters = { + docs: { + source: { + code: tagToString('line-chart', TwoSeries.args), + }, + }, +}; + +export const TwoSeriesWithPlotPoints = Template.bind({}); +TwoSeriesWithPlotPoints.args = { + series: sampleSeriesData.slice(0, 2), + plotPoints: true, +}; + +TwoSeriesWithPlotPoints.parameters = { + docs: { + source: { + code: tagToString('line-chart', TwoSeriesWithPlotPoints.args), + }, + }, +}; + +export const ThreeSeries = Template.bind({}); +ThreeSeries.args = { + series: sampleSeriesData.slice(0, 3), +}; + +ThreeSeries.parameters = { + docs: { + source: { + code: tagToString('line-chart', ThreeSeries.args), + }, + }, +}; + +export const ThreeSeriesWithPlotPoints = Template.bind({}); +ThreeSeriesWithPlotPoints.args = { + series: sampleSeriesData.slice(0, 3), + plotPoints: true, +}; + +ThreeSeriesWithPlotPoints.parameters = { + docs: { + source: { + code: tagToString('line-chart', ThreeSeriesWithPlotPoints.args), + }, + }, +}; + +export const FourSeries = Template.bind({}); +FourSeries.args = { + series: sampleSeriesData.slice(0, 4), +}; + +FourSeries.parameters = { + docs: { + source: { + code: tagToString('line-chart', FourSeries.args), + }, + }, +}; + +export const FourSeriesWithPlotPoints = Template.bind({}); +FourSeriesWithPlotPoints.args = { + series: sampleSeriesData.slice(0, 4), + plotPoints: true, +}; + +FourSeriesWithPlotPoints.parameters = { + docs: { + source: { + code: tagToString('line-chart', FourSeriesWithPlotPoints.args), + }, + }, +}; + +export const FiveSeries = Template.bind({}); +FiveSeries.args = { + series: sampleSeriesData.slice(0), +}; + +FiveSeries.parameters = { + docs: { + source: { + code: tagToString('line-chart', FiveSeries.args), + }, + }, +}; + +export const FiveSeriesWithPlotPoints = Template.bind({}); +FiveSeriesWithPlotPoints.args = { + series: sampleSeriesData.slice(0), + plotPoints: true, +}; + +FiveSeriesWithPlotPoints.parameters = { + docs: { + source: { + code: tagToString('line-chart', FiveSeriesWithPlotPoints.args), + }, + }, +}; diff --git a/src/components/ebay-line-chart/marko-tag.json b/src/components/ebay-line-chart/marko-tag.json new file mode 100644 index 000000000..843e2d777 --- /dev/null +++ b/src/components/ebay-line-chart/marko-tag.json @@ -0,0 +1,23 @@ +{ + "attribute-groups": ["html-attributes"], + "@*": { + "targetProperty": null, + "type": "expression" + }, + "@html-attributes": "expression", + "@title": "string", + "@description": "string", + "@series[]": { + "@title": "string", + "@data[]": { + "@x": "number", + "@y": "number", + "@label": "string" + } + }, + "@xAxisLabelFormat": "string", + "@xAxisPositioner": "function", + "@yAxisLabels": "array", + "@yAxisPositioner": "function", + "@trend": "boolean" +} diff --git a/src/components/ebay-line-chart/style.less b/src/components/ebay-line-chart/style.less new file mode 100644 index 000000000..0315d7d83 --- /dev/null +++ b/src/components/ebay-line-chart/style.less @@ -0,0 +1,31 @@ +.ebay-line-chart { + width: 100%; +} + +.ebay-line-chart__container { + height: 300px; +} + +.ebay-line-chart .highcharts-point { + opacity: 0; // overriding the default highcharts opacity to 0 so we can control it + stroke-width: 2px; // and adjusting the stroke width; +} + +.ebay-line-chart .ebay-line-chart__marker--visible { + opacity: 1; +} + +.ebay-line-chart .highcharts-legend-item .highcharts-point { + opacity: 1; // displays the legend marker symbols which are hidden by the highcharts-point opacity + stroke-width: 0; // hide the default stroke +} + +.ebay-line-chart .highcharts-legend-item .highcharts-graph { + display: none; // this hides a line strike through the marker symbols that we do not want +} + +@media (prefers-color-scheme: dark) { + .ebay-line-chart .highcharts-halo { + fill-opacity: 0.8; // this inverses the default 0.2 fill opacity in from light mode + } +} diff --git a/src/components/ebay-line-chart/tooltip.marko b/src/components/ebay-line-chart/tooltip.marko new file mode 100644 index 000000000..fa23802da --- /dev/null +++ b/src/components/ebay-line-chart/tooltip.marko @@ -0,0 +1,13 @@ +${input.date} +
+ + $ var point = pts.point; + ${point.tooltip} + +
+ ${pnt.series.name} + ${point.label} +
+ + ${point.label} + diff --git a/src/components/ebay-spark-line/README.md b/src/components/ebay-spark-line/README.md new file mode 100644 index 000000000..1f5a27c1c --- /dev/null +++ b/src/components/ebay-spark-line/README.md @@ -0,0 +1,10 @@ +

+ + ebay-spark-line + + + DS v3.7.0 + +

+ +The spark line displays data points as a static svg graphic diff --git a/src/components/ebay-spark-line/component.js b/src/components/ebay-spark-line/component.js new file mode 100644 index 000000000..d7cdb94e0 --- /dev/null +++ b/src/components/ebay-spark-line/component.js @@ -0,0 +1,56 @@ +export default { + getSparkLinePath() { + // path is used to compile the commands to draw the spark line svg + // refer to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d + const path = []; + const minMax = this.getMinMax(); + const normalizationScaleRatios = this.getNormalizationScaleRatios(minMax); + + // loop through each data point to generate the path string' + this.input.data.forEach((p, i) => { + // set the command, either M for move to on the first data point or L for line + path.push(i === 0 ? 'M ' : 'L'); + // set the x value minus the startX times the normlize ratio + 6 for padding and line width + path.push((p.x - minMax.startX) * normalizationScaleRatios.xScale + 6); + // set the y value minus the minimum y value times the normalize ratio + 10 padding and line width + path.push(120 - ((p.y - minMax.yMin) * normalizationScaleRatios.yScale + 10)); + }); + return path.join(' '); // join the path array into a string with spaces for use in the path d attribute + }, + getMinMax() { + // get the start and end x values + const startX = this.input.data[0].x; + const endX = this.input.data[this.input.data.length - 1].x; + + // find the max and min y values + let yMax; + let yMin; + this.input.data.forEach((p) => { + yMax = !yMax || p.y > yMax ? p.y : yMax; + yMin = !yMin || p.y < yMin ? p.y : yMin; + }); + return { + startX, + endX, + yMin, + yMax, + }; + }, + getNormalizationScaleRatios(minMax) { + // calculate the x and y scale ratios to normalize data + const yScale = 100 / (minMax.yMax - minMax.yMin); + const xScale = 100 / (minMax.endX - minMax.startX); + return { + xScale, + yScale, + }; + }, + getViewBox() { + const minMax = this.getMinMax(); + const normalizationScaleRatios = this.getNormalizationScaleRatios(minMax); + // set the viewBox x, y, width, height + return `0 0 ${(minMax.endX - minMax.startX) * normalizationScaleRatios.xScale + 12} ${ + (minMax.yMax - minMax.yMin) * normalizationScaleRatios.yScale + 20 + }`; + }, +}; diff --git a/src/components/ebay-spark-line/examples/data.json b/src/components/ebay-spark-line/examples/data.json new file mode 100644 index 000000000..508519f93 --- /dev/null +++ b/src/components/ebay-spark-line/examples/data.json @@ -0,0 +1,136 @@ +{ + "basic": { + "data": [ + { + "x": 1643673600000, + "y": 1901.5 + }, + { + "x": 1643760000000, + "y": 225.61 + }, + { + "x": 1643846400000, + "y": 1552.68 + }, + { + "x": 1643932800000, + "y": 2611.25 + }, + { + "x": 1644019200000, + "y": 304.87 + }, + { + "x": 1644105600000, + "y": 1188.05 + }, + { + "x": 1644192000000, + "y": 577.5 + }, + { + "x": 1644278400000, + "y": -1510.93 + }, + { + "x": 1644364800000, + "y": -3530.1 + }, + { + "x": 1644451200000, + "y": -2601.05 + } + ] + }, + "positive": { + "data": [ + { + "x": 1643673600000, + "y": -921.75 + }, + { + "x": 1643760000000, + "y": -571.82 + }, + { + "x": 1643846400000, + "y": 1567.05 + }, + { + "x": 1643932800000, + "y": 2811.23 + }, + { + "x": 1644019200000, + "y": 4453.02 + }, + { + "x": 1644105600000, + "y": 4657.16 + }, + { + "x": 1644192000000, + "y": 4858.55 + }, + { + "x": 1644278400000, + "y": 4574.2 + }, + { + "x": 1644364800000, + "y": 3017.05 + }, + { + "x": 1644451200000, + "y": 1756.36 + } + ], + "trend": "positive" + }, + "negative": { + "data": [ + { + "x": 1643673600000, + "y": 1658.83 + }, + { + "x": 1643760000000, + "y": 3232.9 + }, + { + "x": 1643846400000, + "y": 1381.91 + }, + { + "x": 1643932800000, + "y": 1020.94 + }, + { + "x": 1644019200000, + "y": 1564.91 + }, + { + "x": 1644105600000, + "y": 1517.78 + }, + { + "x": 1644192000000, + "y": 946.39 + }, + { + "x": 1644278400000, + "y": 537.55 + }, + { + "x": 1644364800000, + "y": 1 + }, + { + "x": 1644451200000, + "y": 53.91 + } + ], + "trend": "negative" + } +} diff --git a/src/components/ebay-spark-line/index.marko b/src/components/ebay-spark-line/index.marko new file mode 100644 index 000000000..8341f7edb --- /dev/null +++ b/src/components/ebay-spark-line/index.marko @@ -0,0 +1,16 @@ + + + diff --git a/src/components/ebay-spark-line/spark-line.stories.js b/src/components/ebay-spark-line/spark-line.stories.js new file mode 100644 index 000000000..37ea28735 --- /dev/null +++ b/src/components/ebay-spark-line/spark-line.stories.js @@ -0,0 +1,67 @@ +import { tagToString } from '../../../.storybook/storybook-code-source'; +import { addRenderBodies } from '../../../.storybook/utils'; +import Readme from './README.md'; +import Component from './index.marko'; +import * as sampleData from './examples/data.json'; + +const Template = (args) => ({ + input: addRenderBodies(args), +}); + +export default { + title: 'charts/ebay-spark-line', + excludeStories: '.*', + component: Component, + parameters: { + docs: { + description: { + component: Readme, + }, + }, + }, + + argTypes: { + data: { + type: { name: 'array', required: true }, + description: 'an array of point objects, each point contains an `x` and `y` value', + }, + trend: { + type: { name: 'string', required: false }, + description: + 'Trend can bet set to `positive` or `negative` to change the color of the line', + }, + }, +}; + +export const Basic = Template.bind({}); +Basic.args = sampleData.basic; + +Basic.parameters = { + docs: { + source: { + code: tagToString('spark-line', Basic.args), + }, + }, +}; + +export const Positive = Template.bind({}); +Positive.args = sampleData.positive; + +Positive.parameters = { + docs: { + source: { + code: tagToString('spark-line', Positive.args), + }, + }, +}; + +export const Negative = Template.bind({}); +Negative.args = sampleData.negative; + +Negative.parameters = { + docs: { + source: { + code: tagToString('spark-line', Negative.args), + }, + }, +}; diff --git a/src/components/ebay-spark-line/style.js b/src/components/ebay-spark-line/style.js new file mode 100644 index 000000000..869d3e4ff --- /dev/null +++ b/src/components/ebay-spark-line/style.js @@ -0,0 +1 @@ +require('./style.less'); diff --git a/src/components/ebay-spark-line/style.less b/src/components/ebay-spark-line/style.less new file mode 100644 index 000000000..f6f8d3932 --- /dev/null +++ b/src/components/ebay-spark-line/style.less @@ -0,0 +1,25 @@ +.ebay-spark-line { + width: 100%; + height: 100%; + max-width: 80px; + max-height: 30px; + transform: translate3d(0, 0, 0); +} + +.ebay-spark-line__path { + fill: none; + stroke-width: 3px; + stroke-linecap: round; +} + +.ebay-spark-line__path-neutral { + stroke: var(--spark-line-neutral, var(--color-data-viz-line-chart-primary)); +} + +.ebay-spark-line__path-positive { + stroke: var(--spark-line-positve, var(--color-data-viz-trend-positive)); +} + +.ebay-spark-line__path-negative { + stroke: var(--spark-line-negative, var(--color-data-viz-trend-negative)); +} diff --git a/src/components/ebay-spark-line/test/__snapshots__/renders-a-blue-line.expected.html b/src/components/ebay-spark-line/test/__snapshots__/renders-a-blue-line.expected.html new file mode 100644 index 000000000..8045c5957 --- /dev/null +++ b/src/components/ebay-spark-line/test/__snapshots__/renders-a-blue-line.expected.html @@ -0,0 +1,17 @@ + +  +  +  + \ No newline at end of file diff --git a/src/components/ebay-spark-line/test/__snapshots__/renders-a-green-line.expected.html b/src/components/ebay-spark-line/test/__snapshots__/renders-a-green-line.expected.html new file mode 100644 index 000000000..0830e7ca6 --- /dev/null +++ b/src/components/ebay-spark-line/test/__snapshots__/renders-a-green-line.expected.html @@ -0,0 +1,17 @@ + +  +  +  + \ No newline at end of file diff --git a/src/components/ebay-spark-line/test/__snapshots__/renders-a-red-line.expected.html b/src/components/ebay-spark-line/test/__snapshots__/renders-a-red-line.expected.html new file mode 100644 index 000000000..a26c6d3d0 --- /dev/null +++ b/src/components/ebay-spark-line/test/__snapshots__/renders-a-red-line.expected.html @@ -0,0 +1,17 @@ + +  +  +  + \ No newline at end of file diff --git a/src/components/ebay-spark-line/test/test.server.js b/src/components/ebay-spark-line/test/test.server.js new file mode 100644 index 000000000..898a13723 --- /dev/null +++ b/src/components/ebay-spark-line/test/test.server.js @@ -0,0 +1,22 @@ +import { use } from 'chai'; +import { composeStories } from '@storybook/marko/dist/testing'; +import { snapshotHTML } from '../../../common/test-utils/snapshots'; +import * as stories from '../spark-line.stories'; // import all stories from the stories file +const { Basic, Positive, Negative } = composeStories(stories); +const htmlSnap = snapshotHTML(__dirname); + +use(require('chai-dom')); + +describe('spark-line', () => { + it('renders a blue line', async () => { + await htmlSnap(Basic); + }); + + it('renders a red line', async () => { + await htmlSnap(Negative); + }); + + it('renders a green line', async () => { + await htmlSnap(Positive); + }); +});