diff --git a/.eslintrc b/.eslintrc index 0d8aad4..69a164d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,8 @@ "plugins": [ "@typescript-eslint", "react", - "react-hooks" + "react-hooks", + "import" ], "rules": { "no-console": "warn", @@ -32,7 +33,21 @@ "prefer-spread": ["off"], "react-hooks/exhaustive-deps": "off", "arrow-parens": "error", - "arrow-spacing": "error" + "arrow-spacing": "error", + "sort-imports": ["error", {"ignoreCase": true, "ignoreDeclarationSort": true}], + "import/order": [ + "error", + { "groups": + [ + "external", + "builtin", + "internal", + "parent", + "sibling", + "index" + ] + } + ] }, "settings": { "react": { diff --git a/.gitignore b/.gitignore index ebbbb77..d410eca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules build dist .rpt2_cache +coverage # misc .DS_Store diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b8ed5cf..08bfd22 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at opensource@hodgef.com. All +reported by contacting the project team at it.team@keyvalue.systems. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6e314b4 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,76 @@ + + + +​ + +## Pull Request Checklist + +​ + +- [ ] **Read the contributing guidelines.** +- [ ] **Linked to an issue:** Fixes # (replace with the issue number, if applicable) +- [ ] **Branch is up-to-date with the base branch:** `main` +- [ ] **Changes pass tests locally:** `npm test` or `yarn test` +- [ ] **Documentation has been updated, if necessary** +- [ ] **Code follows the style guide of the project** + ​ + +## Description + +​ + + + +​ + +## Screenshots (if applicable) + +​ + + + +​ + +## Additional Notes + +​ + + + +​ + +## Related Issues or PRs + +​ + + + +​ + +## Reviewer Guidelines + +​ + + + +​ + +## Testing Instructions + +​ + + + +​ + +## Checklist for Reviewers + +​ + +- [ ] Code follows project conventions and style +- [ ] Changes do not introduce new warnings or errors +- [ ] Unit tests cover the changes +- [ ] Documentation is updated + ​ + +## By submitting this pull request, I confirm that my contribution is made under the terms of the MIT License. diff --git a/README.md b/README.md index ab4a05b..9cf5a31 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ - - - - # React Dot Matrix Chart npm version @@ -10,10 +6,9 @@ - ->A customizable ready to use Dot Matrix Chart Component for React +> A customizable ready to use Dot Matrix Chart Component for React -Try tweaking a dot matrix using this codesandbox link here +Try tweaking a dot matrix using this codesandbox link here ## Installation @@ -25,53 +20,42 @@ npm install @keyvaluesystems/react-dot-matrix-chart You’ll need to install React separately since it isn't included in the package. - - ## Usage - - React Dot Matrix Chart can run in a very basic mode by just providing the `dataPoints` like given below: ```jsx +import DotMatrix from "@keyvaluesystems/react-dot-matrix-chart"; -import DotMatrix from '@keyvaluesystems/react-dot-matrix-chart'; - - - +; ``` The datapoints is an array of objects with the following keys: -- `name` - a string that represents each category -- `count` - a number to specify the count of each category present(used to find the number of dots to be displayed) -- `color` - a string to specify which colour to be used to represent the category in the dot matrix - - +- `name` - a string that represents each category +- `count` - a number to specify the count of each category present(used to find the number of dots to be displayed) +- `color` - a string to specify which colour to be used to represent the category in the dot matrix An example for dataPoints array is shown below: ```jsx const dataPointsArray = [ { - name: 'Category 1', + name: "Category 1", count: 10, - color: 'gray' + color: "gray" }, { - name: 'Category 2', + name: "Category 2", count: 10, - color: 'black' + color: "black" }, { - name: 'Category 3', + name: "Category 3", count: 10, - color: 'green' + color: "green" } ]; - ``` You can specify the number of rows or columns to be present in the chart as well using the dimensions prop. @@ -79,31 +63,21 @@ You can specify the number of rows or columns to be present in the chart as well ```jsx ``` If the count given in the dataPoints array results in a partial percentage (decimal value), a gradient dot will be displayed as shown below +
-We can also control the display of the legend consisting of the details regarding the colour distribution using the props 'showLegend' and 'legendPosition' as follows. - -```jsx - -``` ## Props - - Props that can be passed to the component are listed below: @@ -130,11 +104,11 @@ Props that can be passed to the component are listed below: - + - + @@ -144,16 +118,22 @@ Props that can be passed to the component are listed below: - + - + + + + + +
{ rows: 5, columns: 12 }
styles?: objectspaceBetweenDots?: number - Provides you with a bunch of callback functions to override the default styles. + To specify the distance between each dot undefined4
showLegend?: booleanfalse
legendPosition?: stringlegendPosition?: 'left' | 'left-start' | 'left-end | 'right' | 'right-start' | 'right-end' | 'top'| 'top-start' | 'top-bottom' | 'bottom' | 'bottom-start' | 'bottom-end' - To specify the position of the legend. The values that can be passed using this prop are 'left', 'right', 'top' and 'bottom' + To specify the position of the legend. rightright-end
styles?: object + Provides you with a bunch of callback functions to override the default styles. + undefined
- ## Style Customizations All the default styles provided by this package are overridable using the `styles` prop. @@ -163,20 +143,19 @@ the below code shows all the overridable styles: ({...styles}), - DotsContainer: () => ({...styles}), - Dot: () => ({...styles}), - LegendContainer: () => ({...styles}), - LegendName: () => ({...styles}), - LegendDot: () => ({...styles}) + Container: () => ({ ...styles }), + DotsContainer: () => ({ ...styles }), + Dot: () => ({ ...styles }), + LegendContainer: () => ({ ...styles }), + LegendName: () => ({ ...styles }), + LegendDot: () => ({ ...styles }) }} /> - ``` -- `Container` - overrides the dot matrix chart container style -- `DotsContainer` - overrides the dot matrix chart dots container style -- `Dot` - overrides the style of each dot in the chart -- `LegendContainer` - overrides the legend (details) container style -- `LegendName` - overrides the legend name style -- `LegendDot` - overrides the legend dot style \ No newline at end of file +- `Container` - overrides the dot matrix chart container style +- `DotsContainer` - overrides the dot matrix chart dots container style +- `Dot` - overrides the style of each dot in the chart +- `LegendContainer` - overrides the legend (details) container style +- `LegendName` - overrides the legend name style +- `LegendDot` - overrides the legend dot style diff --git a/STYLE_GUIDELINES.md b/STYLE_GUIDELINES.md new file mode 100644 index 0000000..7781de0 --- /dev/null +++ b/STYLE_GUIDELINES.md @@ -0,0 +1,23 @@ +## SCSS Style Guidelines for @keyvaluesystems/react-dot-matrix-chart + +**Introduction** + +As an open-source project utilizing SCSS, @keyvaluesystems/react-dot-matrix-chart strives to maintain a consistent and well-structured codebase. These SCSS style guidelines serve as a reference for contributors, ensuring that their SCSS code adheres to established conventions and best practices. + +**SCSS Coding Conventions** + +- Organize SCSS files into a logical structure. +- Use meaningful and descriptive names for variables, mixins, and classes. +- Use SCSS nesting judiciously to organize complex styles. +- Include comments to explain non-obvious logic and complex styles. +- Utilize SCSS variables to define reusable values. +- Employ a SCSS linting tool. +- Should support devices with all resolutions. +- Follow CamelCase conventions for class names that concisely convey their purpose, enhancing code organization and readability. +- Adhere to the practice of reusing style classes to improve code organization and maintainability. + +**Documentation Practices** + +- Provide clear documentation for exported mixins and variables. +- Include a README file within the SCSS directory if necessary. +- Add comments to SCSS files. diff --git a/package-lock.json b/package-lock.json index e3cdcc8..993fb25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,6 @@ "ts-loader": "^9.4.2", "typescript": "^4.9.5", "url-loader": "^4.1.1", - "uuid": "^9.0.0", "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-server": "4.11.1", @@ -33928,14 +33927,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "9.0.0", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/uuid-browser": { "version": "3.1.0", "dev": true, diff --git a/package.json b/package.json index b90c254..d8c07ef 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "ts-loader": "^9.4.2", "typescript": "^4.9.5", "url-loader": "^4.1.1", - "uuid": "^9.0.0", "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-server": "4.11.1", @@ -92,6 +91,9 @@ "eslintConfig": { "extends": "./.eslintrc.json" }, + "prettier": { + "trailingComma": "none" + }, "jest": { "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/scripts/testMock.js", diff --git a/src/lib/dot-matrix/Chart.tsx b/src/lib/dot-matrix/Chart.tsx index d6c8bd4..251e056 100644 --- a/src/lib/dot-matrix/Chart.tsx +++ b/src/lib/dot-matrix/Chart.tsx @@ -1,76 +1,77 @@ -import React, { useMemo } from 'react'; -import { v4 } from 'uuid'; -import { - getNumberOfDots, - getStyles, - hasOverlapping -} from './utils/utils'; +import React, { useMemo } from "react"; + +import { getNumberOfDots, getStyles, hasOverlapping } from "./utils/utils"; import { - Elements, DEFAULT_COLUMNS, + DEFAULT_DOT_WIDTH, DEFAULT_ROWS, - DEFAULT_ROW_WIDTH, - DEFAULT_ROW_GAP -} from './constants'; -import { ChartProps, DataPointType } from './types'; -import classes from './styles.module.scss'; + Elements +} from "./constants"; +import { ChartProps, DataPointType } from "./types"; +import classes from "./styles.module.scss"; -const Chart = (props: ChartProps) : JSX.Element => { +const Chart = (props: ChartProps): JSX.Element => { const { dimensions = {}, styles, - data, - overlappingValues, + dotsToBeMapped, + fractionalDots, total, - width + width, + spaceBetweenDots } = props; - const { - rows = DEFAULT_ROWS, - columns = DEFAULT_COLUMNS - } = dimensions; + + const { rows = DEFAULT_ROWS, columns = DEFAULT_COLUMNS } = dimensions; const dotWidth = useMemo( - () => (width ? width / columns - DEFAULT_ROW_GAP : DEFAULT_ROW_WIDTH), + () => (width ? width / columns - spaceBetweenDots : DEFAULT_DOT_WIDTH), [width] ); + return (
- {data?.map((dataItem: DataPointType, rowIndex: number) => ( - - {dataItem && Array.apply(null, Array(getNumberOfDots(dataItem, rows, columns, total))).map((item: null, columnIndex: number) => ( -
- {(hasOverlapping(overlappingValues, rowIndex, columnIndex) && ( -
- )) || ( + {dotsToBeMapped?.map((dataItem: DataPointType, rowIndex: number) => ( + + {dataItem && + [...Array(getNumberOfDots(dataItem, rows, columns, total))].map( + (item: null, columnIndex: number) => (
- )} -
- ))} + id="dot-matrix-dots" + key={`${dataItem.name}-${rowIndex}-${columnIndex}`} + > + {hasOverlapping(fractionalDots, rowIndex, columnIndex) ? ( +
+ ) : ( +
+ )} +
+ ) + )} ))}
diff --git a/src/lib/dot-matrix/DotMatrix.tsx b/src/lib/dot-matrix/DotMatrix.tsx index 8af4f72..cc744f4 100644 --- a/src/lib/dot-matrix/DotMatrix.tsx +++ b/src/lib/dot-matrix/DotMatrix.tsx @@ -1,17 +1,20 @@ -import React from 'react'; -import classes from './styles.module.scss'; -import { DotMatrixPropType } from './types'; -import { useDotMatrix } from './custom-hooks/useDotMatrix'; -import useChartContainerWidth from './custom-hooks/useChartContainerWidth'; -import Chart from './Chart'; -import Legend from './Legend'; -import { getLegendPosition, getStyles } from './utils/utils'; +import React from "react"; + +import { useDotMatrix } from "./custom-hooks/useDotMatrix"; +import { getLegendPosition, getStyles } from "./utils/utils"; import { - Elements, DEFAULT_COLUMNS, + DEFAULT_GAP, + DEFAULT_LEGEND_POSITION, DEFAULT_ROWS, - DEFAULT_LEGEND_POSITION -} from './constants'; + Elements +} from "./constants"; +import { DotMatrixPropType } from "./types"; +import Chart from "./Chart"; +import Legend from "./Legend"; +import useChartContainerWidth from "./custom-hooks/useChartContainerWidth"; +import classes from "./styles.module.scss"; + const DotMatrix = (props: DotMatrixPropType): JSX.Element => { const { dataPoints, @@ -19,16 +22,21 @@ const DotMatrix = (props: DotMatrixPropType): JSX.Element => { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS }, + spaceBetweenDots = DEFAULT_GAP, styles = {}, showLegend = false, legendPosition = DEFAULT_LEGEND_POSITION } = props; - const width = useChartContainerWidth('dots-container', [ + const width = useChartContainerWidth("dots-container", [ showLegend, legendPosition ]); - const [data, total, overlappingValues] = useDotMatrix(dataPoints, dimensions); + + const [dotsToBeMapped, totalDots, fractionalDots] = useDotMatrix( + dataPoints, + dimensions + ); return (
@@ -43,17 +51,14 @@ const DotMatrix = (props: DotMatrixPropType): JSX.Element => {
- {showLegend && ( -
- -
- )} + {showLegend && }
); diff --git a/src/lib/dot-matrix/Legend.tsx b/src/lib/dot-matrix/Legend.tsx index 35c0303..77a02e1 100644 --- a/src/lib/dot-matrix/Legend.tsx +++ b/src/lib/dot-matrix/Legend.tsx @@ -1,41 +1,38 @@ -import React from 'react'; -import { v4 } from 'uuid'; -import { getStyles } from './utils/utils'; +import React from "react"; + +import { getStyles } from "./utils/utils"; import { DataPointType, LegendProps } from "./types"; -import { - Elements -} from './constants'; -import classes from './styles.module.scss'; +import { Elements } from "./constants"; +import classes from "./styles.module.scss"; const Legend = (props: LegendProps): JSX.Element => { - const { - styles, - data - } = props; + const { styles, data } = props; + return (
- {data?.map((point: DataPointType) => ( -
+ {data?.map((point: DataPointType, index) => ( +
{point.name}
))}
- ) + ); }; export default Legend; \ No newline at end of file diff --git a/src/lib/dot-matrix/constants.ts b/src/lib/dot-matrix/constants.ts index 0851001..7d4dd48 100644 --- a/src/lib/dot-matrix/constants.ts +++ b/src/lib/dot-matrix/constants.ts @@ -1,13 +1,13 @@ -export const COLOR_PALATTE = [ - '#fd7f6f', - '#7eb0d5', - '#b2e061', - '#bd7ebe', - '#ffb55a', - '#ffee65', - '#beb9db', - '#fdcce5', - '#8bd3c7' +export const COLOR_PALETTE = [ + "#fd7f6f", + "#7eb0d5", + "#b2e061", + "#bd7ebe", + "#ffb55a", + "#ffee65", + "#beb9db", + "#fdcce5", + "#8bd3c7" ]; export enum Elements { @@ -23,8 +23,10 @@ export const DEFAULT_ROWS = 5; export const DEFAULT_COLUMNS = 12; -export const DEFAULT_LEGEND_POSITION = 'right'; +export const DEFAULT_LEGEND_POSITION = "right-end"; -export const DEFAULT_ROW_WIDTH = 35; +export const DEFAULT_DOT_WIDTH = 10; -export const DEFAULT_ROW_GAP = 4; +export const DEFAULT_GAP = 4; + +export const DEFAULT_DOT_CONTAINER_WIDTH = 600; diff --git a/src/lib/dot-matrix/custom-hooks/useChartContainerWidth.ts b/src/lib/dot-matrix/custom-hooks/useChartContainerWidth.ts index 0ab431a..18fa9e1 100644 --- a/src/lib/dot-matrix/custom-hooks/useChartContainerWidth.ts +++ b/src/lib/dot-matrix/custom-hooks/useChartContainerWidth.ts @@ -1,11 +1,13 @@ -import { useState, useEffect } from 'react'; -import { findContainerWidth } from '../utils/utils'; +import { useEffect, useState } from "react"; + +import { DEFAULT_DOT_CONTAINER_WIDTH } from "../constants"; +import { findContainerWidth } from "../utils/utils"; const useChartContainerWidth = ( id: string, dependencyArray: Array ): number => { - const [width, setWidth] = useState(0); + const [width, setWidth] = useState(DEFAULT_DOT_CONTAINER_WIDTH); useEffect(() => { updateContainerWidth(); @@ -15,12 +17,19 @@ const useChartContainerWidth = ( updateContainerWidth(); }, [...dependencyArray]); - window.onresize = (): void => { - updateContainerWidth(); - }; + useEffect(() => { + if (typeof window !== "undefined") { + window.addEventListener("resize", updateContainerWidth); + + return () => { + window.removeEventListener("resize", updateContainerWidth); + }; + } + }, []); const updateContainerWidth = (): void => { - const widthValue = findContainerWidth(id); + let widthValue; + if (typeof window !== "undefined") widthValue = findContainerWidth(id); if (widthValue) setWidth(widthValue); }; return width; diff --git a/src/lib/dot-matrix/custom-hooks/useDotMatrix.ts b/src/lib/dot-matrix/custom-hooks/useDotMatrix.ts index 7860c40..59d863c 100644 --- a/src/lib/dot-matrix/custom-hooks/useDotMatrix.ts +++ b/src/lib/dot-matrix/custom-hooks/useDotMatrix.ts @@ -1,42 +1,47 @@ -import { useMemo} from "react"; -import { DataPointType } from '../types'; -import { COLOR_PALATTE, DEFAULT_COLUMNS , DEFAULT_ROWS } from '../constants'; -import { isColorPresent } from "../utils/utils"; +import { useMemo } from "react"; -export const useDotMatrix = (dataPoints: DataPointType[], dimensions: { rows?: number, columns?: number }): [DataPointType[], number, number[]] => { +import { DataPointType, DimensionProp } from "../types"; +import { COLOR_PALETTE, DEFAULT_COLUMNS, DEFAULT_ROWS } from "../constants"; +import { isColorAlreadyUsed } from "../utils/utils"; - const [data, total] = useMemo(() => { - const values: DataPointType[] = []; - let totalVal = 0 +export const useDotMatrix = ( + dataPoints: DataPointType[], + dimensions: DimensionProp +): [DataPointType[], number, number[]] => { + const [dotsToBeMapped, totalDots] = useMemo(() => { + const dotMatrixData: DataPointType[] = []; + let totalCount = 0; if (dataPoints) { let colorIndex = 0; dataPoints.forEach((point: DataPointType) => { - totalVal += point.count; + totalCount += point.count; let { color } = point; if (!color) { do { - color = COLOR_PALATTE[colorIndex]; + color = COLOR_PALETTE[colorIndex]; colorIndex++; - } while (isColorPresent(dataPoints, color, values)) + } while (isColorAlreadyUsed(dataPoints, color, dotMatrixData)); } - values.push({ ...point, color }); - }) + dotMatrixData.push({ ...point, color }); + }); } - return [values, totalVal] + return [dotMatrixData, totalCount] }, [dataPoints]); - const overlappingValues: number[] = useMemo(() => { - const partial: Array = []; - if (total) { - data?.forEach((each: DataPointType) => { + // Calculates fractional part of a category based on the provided data points + // relative to the total number of dots and dimension + const fractionalDots: number[] = useMemo(() => { + const fractionalParts: Array = []; + if (totalDots) { + dotsToBeMapped?.forEach((point: DataPointType) => { const { rows = DEFAULT_ROWS, columns = DEFAULT_COLUMNS } = dimensions; - const percentage = each.count / total; - const partialDots = percentage * rows * columns; - const value = partialDots - Math.floor(partialDots); - partial.push(value); - }) + const pointPercentage = point.count / totalDots; + const dotsCount = pointPercentage * rows * columns; + const dotFraction = dotsCount - Math.floor(dotsCount); + fractionalParts?.push(dotFraction); + }); } - return partial; - }, [total]); - return [data, total, overlappingValues]; -} \ No newline at end of file + return fractionalParts; + }, [totalDots]); + return [dotsToBeMapped, totalDots, fractionalDots]; +}; \ No newline at end of file diff --git a/src/lib/dot-matrix/styles.module.scss b/src/lib/dot-matrix/styles.module.scss index aacc5dd..c2fabc4 100644 --- a/src/lib/dot-matrix/styles.module.scss +++ b/src/lib/dot-matrix/styles.module.scss @@ -20,6 +20,7 @@ } .legendDot { border-radius: 50%; + flex-shrink: 0; width: 1.6rem; height: 1.6rem; } @@ -40,4 +41,16 @@ padding: 7px; width: max-content; } +} +@media (max-width: 768px) { + .container{ + .legendDot { + width: 0.6rem; + height: 0.6rem; + } + .legend .name { + font-size: 8px; + } + } + } \ No newline at end of file diff --git a/src/lib/dot-matrix/types.d.ts b/src/lib/dot-matrix/types.d.ts index d469883..108fd8c 100644 --- a/src/lib/dot-matrix/types.d.ts +++ b/src/lib/dot-matrix/types.d.ts @@ -1,39 +1,54 @@ export interface DataPointType { - name: string, - count: number, - color?: string + name: string; + count: number; + color?: string; } export interface ChartProps { - dimensions?: DimensionProp, - styles: StyleProp, - data: DataPointType[], - overlappingValues: number[], - total: number, - width: number + dimensions?: DimensionProp; + styles: StyleProp; + dotsToBeMapped: DataPointType[]; + fractionalDots: number[]; + total: number; + width: number; + spaceBetweenDots: number; } export type DimensionProp = { rows?: number; - columns?: number -} + columns?: number; +}; export type StyleProp = { - Dot?: () => {}, - DotsContainer?: () => {}, - Container?: () => {}, - LegendContainer?: () => {}, - LegendName?: () => {}, - LegendDot?: () => {} -} + Dot?: () => {}; + DotsContainer?: () => {}; + Container?: () => {}; + LegendContainer?: () => {}; + LegendName?: () => {}; + LegendDot?: () => {}; +}; + export interface LegendProps { - styles: StyleProp, - data: DataPointType[] + styles: StyleProp; + data: DataPointType[]; } + export interface DotMatrixPropType { - dataPoints: DataPointType[], - dimensions?: DimensionProp, - showLegend?: boolean, - legendPosition?: 'left' | 'right' | 'top' | 'bottom' - styles?: StyleProp + dataPoints: DataPointType[]; + dimensions?: DimensionProp; + spaceBetweenDots?: number; + showLegend?: boolean; + legendPosition?: + | "left" + | "left-start" + | "left-end" + | "right" + | "right-start" + | "right-end" + | "top" + | "top-start" + | "top-end" + | "bottom" + | "bottom-start" + | "bottom-end"; + styles?: StyleProp; } - diff --git a/src/lib/dot-matrix/utils/utils.ts b/src/lib/dot-matrix/utils/utils.ts index c00f63e..0f0217a 100644 --- a/src/lib/dot-matrix/utils/utils.ts +++ b/src/lib/dot-matrix/utils/utils.ts @@ -1,32 +1,43 @@ import { DataPointType, StyleProp } from "../types"; -import { Elements } from '../constants'; -export const getNumberOfDots = (point: DataPointType, rows: number, columns: number, total: number): number => { +import { Elements } from "../constants"; + +export const getNumberOfDots = ( + point: DataPointType, + rows: number, + columns: number, + total: number +): number => { const percentage = point.count / total; return Math.floor(percentage * rows * columns); -} +}; -export const isColorPresent = (dataPoints: DataPointType[], color: string, dataValues: DataPointType[] = []): boolean => { +export const isColorAlreadyUsed = ( + dataPoints: DataPointType[], + color: string, + dataValues: DataPointType[] = [] +): boolean => { const findColor = dataPoints?.find((e) => e.color === color); const colorInLocal = dataValues?.find((e) => e.color === color); return Boolean(findColor) || Boolean(colorInLocal); -} +}; + +export const getLegendPosition = ( + legendPosition: string +): { flexDirection: string; alignItems: string } => { + const [position, alignment] = legendPosition.split("-"); -export const getLegendPosition = (legendPosition: string): {flexDirection: string, alignItems: string} => { const flexDirection = { - left: 'row-reverse', - right: 'row', - top: 'column-reverse', - bottom: 'column' + left: "row-reverse", + right: "row", + top: "column-reverse", + bottom: "column" }; - const alignment = { - left: 'end', - right: 'end', - top: 'center', - bottom: 'center' - } - return { flexDirection: flexDirection[legendPosition], alignItems: alignment[legendPosition]}; -} + return { + flexDirection: flexDirection[position], + alignItems: alignment ? alignment : "center" + }; +}; export const getStyles = (element: Elements, styles: StyleProp): object => { const getElementStyle = styles[element]; @@ -36,15 +47,21 @@ export const getStyles = (element: Elements, styles: StyleProp): object => { return {}; }; -export const hasOverlapping = (values: number[], indexRow: number, indexColumn: number): boolean => ( - indexColumn === 0 && indexRow > 0 && values[indexRow - 1] < 1 && values[indexRow - 1] !== 0 -); +export const hasOverlapping = ( + values: number[], + indexRow: number, + indexColumn: number +): boolean => + indexColumn === 0 && + indexRow > 0 && + values[indexRow - 1] < 1 && + values[indexRow - 1] !== 0; -export const findContainerWidth = (id: string):number => { +export const findContainerWidth = (id: string): number => { const element = document.getElementById(id); let value = 0; if (element) { value = element.clientWidth; } return value; -} \ No newline at end of file +}; diff --git a/src/lib/tests/dotMatrix.test.tsx b/src/lib/tests/dotMatrix.test.tsx index 511df04..8b9849b 100644 --- a/src/lib/tests/dotMatrix.test.tsx +++ b/src/lib/tests/dotMatrix.test.tsx @@ -1,29 +1,32 @@ -import React from 'react'; +import React from "react"; import { render, - fireEvent, queryByAttribute, queryAllByAttribute } from "@testing-library/react"; -import DotMatrix from '../dot-matrix'; -import { DotMatrixPropType } from '../dot-matrix/types'; +import "@testing-library/jest-dom"; -const getById = queryByAttribute.bind(null, 'id'); -const getAllById = queryAllByAttribute.bind(null, 'id'); +import { DotMatrixPropType } from "../dot-matrix/types"; +import { getStyles, hasOverlapping } from "../dot-matrix/utils/utils"; +import { Elements } from "../dot-matrix/constants"; +import DotMatrix from "../dot-matrix"; + +const getById = queryByAttribute.bind(null, "id"); +const getAllById = queryAllByAttribute.bind(null, "id"); test("If all categories are displayed in Dot Matrix Chart", async () => { const dotMatrixProps: DotMatrixPropType = { dataPoints: [ { - name: 'Category 1', + name: "Category 1", count: 10 }, { - name: 'Category 2', + name: "Category 2", count: 10 }, { - name: 'Categroy 3', + name: "Categroy 3", count: 10 } ] @@ -33,7 +36,8 @@ test("If all categories are displayed in Dot Matrix Chart", async () => { const category1 = await getById(dom.container, "each-category-0-0"); const category2 = await getById(dom.container, "each-category-1-0"); const category3 = await getById(dom.container, "each-category-2-0"); - if (!category1 || !category2 || !category3) throw Error("All Categories not present"); + if (!category1 || !category2 || !category3) + throw Error("All Categories not present"); } else { throw Error("No DOM Found"); } @@ -41,17 +45,19 @@ test("If all categories are displayed in Dot Matrix Chart", async () => { test("If the color is used in the Dot Matrix Chart", async () => { const dotMatrixProps: DotMatrixPropType = { - dataPoints: [{ - name: 'Category 1', - count: 12, - color: 'black' - }] - } + dataPoints: [ + { + name: "Category 1", + count: 12, + color: "black" + } + ] + }; const dom = render(); if (dom) { const dot = await getById(dom.container, "each-category-0-0"); if (!dot) throw Error("Dot Absent"); - expect(dot.style._values["background-color"]).toBe('black') + expect(dot.style._values["background-color"]).toBe("black"); } else { throw Error("No DOM Found"); } @@ -59,22 +65,64 @@ test("If the color is used in the Dot Matrix Chart", async () => { test("If the number of dots rendered in Dot Matrix Chart is correct", async () => { const dotMatrixProps: DotMatrixPropType = { - dataPoints: [{ - name: 'Category 1', - count: 12, - color: 'black' - }], + dataPoints: [ + { + name: "Category 1", + count: 12, + color: "black" + } + ], dimensions: { rows: 5, columns: 5 } - } + }; const dom = render(); if (dom) { const dot = await getAllById(dom.container, "dot-matrix-dots"); if (dot?.length === 0) throw Error("Dots Absent"); - expect(dot.length).toBe(25) + expect(dot.length).toBe(25); } else { throw Error("No DOM Found"); } }); + +test("renders DotMatrix with legend when showLegend is true", () => { + const dotMatrixProps: DotMatrixPropType = { + dataPoints: [ + { + name: "Category 1", + count: 12, + color: "black" + } + ], + showLegend: true + }; + const { container } = render(); + + const legendContainer = getById(container, "legend-container"); + expect(legendContainer).toBeInTheDocument(); +}); + +test("getStyles util should return an empty object when no styles are provided", () => { + const result = getStyles(Elements.Container, {}); + expect(result).toEqual({}); +}); + +test("getStyles util should return the style object for a specific element if available", async () => { + const mockStyle = { color: "red", fontSize: "16px" }; + const styles = { + [Elements.DotsContainer]: () => mockStyle, + [Elements.Dot]: () => ({}) + }; + const result = getStyles(Elements.DotsContainer, styles); + expect(result).toEqual(mockStyle); +}); + +test("should return true when [indexRow - 1] is not equal to 0 and less than 1", () => { + const values = [2, 0.5, 1]; + const indexRow = 2; + const indexColumn = 0; + const result = hasOverlapping(values, indexRow, indexColumn); + expect(result).toBe(true); +}); diff --git a/src/stories/Component.stories.tsx b/src/stories/Component.stories.tsx index 8967219..b332980 100644 --- a/src/stories/Component.stories.tsx +++ b/src/stories/Component.stories.tsx @@ -1,48 +1,133 @@ -import React from 'react'; -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import Component from '../lib'; +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import Component from "../lib"; export default { - title: 'Storybook/Dot Matrix Chart', - component: Component, - parameters: { - // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout - layout: 'fullscreen', - }, - } as ComponentMeta; + title: "Storybook/Dot Matrix Chart", + component: Component, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +} as ComponentMeta; - const Template: ComponentStory = (args) => ; +const Template: ComponentStory = (args) => ( + +); export const DotMatrixExample = Template.bind({}); DotMatrixExample.args = { dimensions: { rows: 5, - columns: 10, + columns: 5 }, - styles:{ + dataPoints: [ + { + name: "Electronics", + count: 25 + }, + { + name: "Fashion", + count: 18 + }, + { + name: "Home & Garden", + count: 12 + }, + { + name: "Sports & Outdoors", + count: 30 + }, + { + name: "Beauty & Personal Care", + count: 15 + } + ] +}; +export const DotMatrixWithCustomStyles = Template.bind({}); +DotMatrixWithCustomStyles.args = { + styles: { + LegendContainer: () => ({ + border: "1px solid white", + width: "60%", + "@media (maxWidth: 768px)": { + width: "50%" + }, + "@media (maxWidth: 480px)": { + width: "40%" + } + }) + }, + dimensions: { + rows: 8, + columns: 8 }, - dataPoints:[ + showLegend: true, + legendPosition: "right", + dataPoints: [ { - name: 'Category 1', + name: "Technology", count: 15, - color: '#96C3EB' + color: "#4CAF50" + }, + { + name: "Healthcare", + count: 5, + color: "#FF5722" + }, + { + name: "Finance", + count: 10, + color: "#2196F3" + }, + { + name: "Education", + count: 10, + color: "#FFC107" + }, + { + name: "Entertainment", + count: 10, + color: "#9C27B0" + } + ] +}; + +export const DotMatrixWithGradientDot = Template.bind({}); +DotMatrixWithGradientDot.args = { + showLegend: true, + dimensions: { + rows: 6, + columns: 5 + }, + legendPosition: "right-start", + dataPoints: [ + { + name: "Art", + count: 21, + color: "#FFD700" }, { - name: 'Category 2', - count: 5 + name: "Science", + count: 23, + color: "#ADD8E6" }, { - name: 'Category 3', - count: 10 + name: "Sports", + count: 24, + color: "#FFA07A" }, { - name: 'Category 3', - count: 10 + name: "Nature", + count: 21, + color: "#98FB98" }, { - name: 'Category 3', - count: 10 + name: "Travel", + count: 25, + color: "#FFB6C1" } ] -}; \ No newline at end of file +};