import "core-js/stable"; import "regenerator-runtime/runtime"; import React, { Component } from "react"; import "./chartify.css"; type Mark = { title: string; xValue: string; yValue: number; }; type Config = { theme: string; width: number; height: number; boxSize: number; boxRadius: number; line: boolean; lineOnly: boolean; bordered: boolean; }; interface ChartifyProps { data: Array; container: string; config: Config; } export default class Chartify extends Component { // get mark classes by mark position getMarkClasses(height: number, i: Mark, aproximateYValue: number) { if (height - aproximateYValue > i.yValue) return "mark empty"; if (height - aproximateYValue === i.yValue) return "mark active"; if (height - aproximateYValue < i.yValue) return "mark painted"; return "mark empty"; } // get basic mark styles object getStyles(config: Config) { const { boxSize = 20, boxRadius = 8, bordered = false } = config; return { width: `${boxSize}px`, height: `${boxSize}px`, borderRadius: `${boxRadius}px`, borderTop: !bordered ? "transparent" : "1px solid rgba(249,250,249, 0.9)", borderLeft: !bordered ? "transparent" : "1px solid rgba(249,250,249, 0.9)" }; } // calculate line styles with calculated incline angle calcLineStyles(currentMark: number, nextMark: number) { const { config: { boxSize = 20 } } = this.props; const AC = boxSize; const BC = Math.abs(nextMark - currentMark) * boxSize; const AB = Math.hypot(AC, BC); const angleA = this.calcLineAngle(BC, AB, nextMark, currentMark); return { width: `${AB}px`, transform: `rotate(${angleA}deg)`, top: `${parseInt(boxSize / 2, 10)}px`, left: `${parseInt(boxSize / 2, 10)}px` }; } // get line incline angle, calculate angle by two sides of imaginary triangle calcLineAngle(BC: number, AB: number, nextMark: number, currentMark: number) { let angleA = Math.fround(Math.asin(BC / AB) * 180 / Math.PI); if (nextMark > currentMark) angleA = -angleA; return angleA; } // calculate approximate max value for the current dataset calculateMaxYValue(dataset: Array) { const valuesArr = dataset.map((el: any) => el.yValue); let maxValue = Math.max.apply(null, valuesArr); maxValue = Math.round(maxValue); let exponent = maxValue.toString().length; exponent -= 1; exponent = !exponent ? 1 : exponent; const prec = 10 ** exponent; maxValue = Math.round(maxValue / prec) * prec; return maxValue; } // render Y-axis with approximate values renderYAxis(column: Array, maxYValue: any) { return (
{column.map((item: Mark, i: number) => (
{i % 2 === 0 ? maxYValue - item.yValue : null}
))}
); } // render tooltip on active mark renderTooltip(mark: Mark, aproximateYValue: number) { const { config: { height } } = this.props; const tooltipStyle = { top: aproximateYValue < height / 2 ? "-100px" : 0 }; return (
{mark.yValue}
{mark.title}
{mark.xValue}
); } // render mark line (if needed) and mark tooltip for active mark renderMarkTools( mark: Mark, drawLine: boolean, aproximateYValue: number, nextAproximateYValue: number ) { const lineStyle = drawLine ? this.calcLineStyles(aproximateYValue, nextAproximateYValue) : null; return ( <> {drawLine ?
: null} {this.renderTooltip(mark, aproximateYValue)} ); } // render column of multiple boxes, each box has own style // possible styles is active, empty, painted renderColumn( dataset: Array, mark: Mark, markNum: number, column: Array, maxY: number ) { const { config } = this.props; const { height = 10, line = false, lineOnly: lineOnly = false } = config; const styles = this.getStyles(config); /* calculate Y-value related to the fixed height of the chart for example: y = 22, height = 10, maxY = 300 aproximateYValue = 1 mark.chart_yValue = 1 */ let aproximateYValue = Math.round(mark.yValue * height / maxY); aproximateYValue = aproximateYValue || 1; return ( <> {column.map((box: any, i: number) => { const markClasses = lineOnly ? "mark white" : this.getMarkClasses(height, box, aproximateYValue); const isActiveMark = height - aproximateYValue === box.yValue && markNum < dataset.length - 1; let nextAproximateYValue = aproximateYValue; if (dataset[markNum + 1]) { nextAproximateYValue = Math.round( dataset[markNum + 1].yValue * height / maxY ); nextAproximateYValue = nextAproximateYValue || 1; } return (
{isActiveMark ? this.renderMarkTools( mark, line || lineOnly, aproximateYValue, nextAproximateYValue ) : null}
); })} ); } // render X-axis with x-values, 1 date for 10 marks renderXAxis(dataset: Array, marksStyle: object) { const { boxSize = 20 } = this.props.config; const showDateCount = dataset.reduce( (prev: any, mark: any, markNum: number) => { const prevPlus = prev + 1; return markNum % 10 === 0 ? prevPlus : prev; }, 0 ); const width: number = parseInt( dataset.length * boxSize / showDateCount, 10 ); const style = { width: `${width}px` }; return (
{dataset.map( (mark: any, markNum: number) => markNum % 10 === 0 ? (
{mark.xValue}
) : null )}
); } // render marks, each mark is a column of multiple boxes renderMarks(dataset: Array, marksStyle: object, maxYValue: number) { const { height } = this.props.config; const column: Array = Array(height) .fill(0) .map((item: number, i: number) => ({ yValue: i })); return (
{dataset.map((mark: any, markNum: any) => (
{this.renderColumn(dataset, mark, markNum, column, maxYValue)}
))}
); } render() { const { data = [], config, container } = this.props; const { height = 10, theme = "default", boxSize = 20 } = config; if (!data || !data.length) { return

No dataset

; } const dataset = data.splice(0, config.width); const maxYValue = this.calculateMaxYValue(dataset); const column = Array(height) .fill(0) .map((item: any, i: any) => { const yValue = Math.round(i * (maxYValue / height)); return { yValue }; }); const rulerClass = `ruler-container ${container} ${theme}`; const width = dataset.length * boxSize; const marksStyle = { width: `${width || 750}px` }; return (
{this.renderYAxis(column, maxYValue)}
{this.renderMarks(dataset, marksStyle, maxYValue)} {this.renderXAxis(dataset, marksStyle)}
); } }