import { group, merge } from "d3-array"; import { ScaleContinuousNumeric } from "d3-scale"; import { stack as d3Stack } from "d3-shape"; import * as React from "react"; import { functor, head, identity, getAxisCanvas, GenericChartComponent, plotDataLengthBarWidth, } from "@fishbot/chart-core"; export interface StackedBarSeriesProps { readonly baseAt?: | number | (( xScale: ScaleContinuousNumeric, yScale: ScaleContinuousNumeric, d: [number, number], moreProps: any, ) => number); readonly clip?: boolean; readonly direction?: "up" | "down"; readonly fillStyle?: string | ((data: any, y: number) => string); readonly spaceBetweenBar?: number; readonly stroke?: boolean; readonly swapScales?: boolean; readonly yAccessor: ((data: any) => number | undefined) | ((d: any) => number)[]; readonly width?: number | ((props: StackedBarSeriesProps, moreProps: any) => number); readonly widthRatio?: number; } export class StackedBarSeries extends React.Component { public static defaultProps = { baseAt: (xScale: ScaleContinuousNumeric, yScale: ScaleContinuousNumeric) => head(yScale.range()), direction: "up", stroke: false, fillStyle: "rgba(70, 130, 180, 0.5)", width: plotDataLengthBarWidth, widthRatio: 0.8, clip: true, swapScales: false, }; public render() { const { clip } = this.props; return ( ); } private readonly drawOnCanvas = (ctx: CanvasRenderingContext2D, moreProps: any) => { const { xAccessor } = moreProps; drawOnCanvasHelper(ctx, this.props, moreProps, xAccessor, d3Stack); }; } export function identityStack() { let keys: any[] = []; function stack(data: any) { const response = keys.map((key, i) => { const arrays = data.map((d: any) => { const array = [0, d[key]]; // @ts-ignore array.data = d; return array; }); arrays.key = key; arrays.index = i; return arrays; }); return response; } stack.keys = function (x: any) { if (!arguments.length) { return keys; } keys = x; return stack; }; return stack; } export function drawOnCanvasHelper( ctx: CanvasRenderingContext2D, props: any, moreProps: any, xAccessor: any, stackFn: any, defaultPostAction = identity, postRotateAction = rotateXY, ) { const { xScale, chartConfig: { yScale }, plotData, } = moreProps; const bars = doStuff(props, xAccessor, plotData, xScale, yScale, stackFn, postRotateAction, defaultPostAction); drawOnCanvas2(props, ctx, bars); } function convertToArray(item: any) { return Array.isArray(item) ? item : [item]; } const doStuff = ( props: StackedBarSeriesProps, xAccessor: any, plotData: any[], xScale: any, yScale: any, stackFn: any, postRotateAction: any, defaultPostAction: any, ) => { const { yAccessor, swapScales } = props; const modifiedYAccessor = swapScales ? convertToArray(xAccessor) : convertToArray(yAccessor); const modifiedXAccessor = swapScales ? yAccessor : xAccessor; const modifiedXScale = swapScales ? yScale : xScale; const modifiedYScale = swapScales ? xScale : yScale; const postProcessor = swapScales ? postRotateAction : defaultPostAction; const bars = getBars( props, modifiedXAccessor, modifiedYAccessor, modifiedXScale, modifiedYScale, plotData, stackFn, postProcessor, ); return bars; }; export const rotateXY = (array: any[]) => array.map((each) => { return { ...each, x: each.y, y: each.x, height: each.width, width: each.height, }; }); export const drawOnCanvas2 = (props: { stroke?: boolean }, ctx: CanvasRenderingContext2D, bars: any) => { const { stroke } = props; const nest = group(bars, (d: any) => d.fillStyle); nest.forEach((values, key) => { if (head(values).width > 1) { if (key !== undefined) { ctx.strokeStyle = key; } } ctx.fillStyle = key; values.forEach((d) => { if (d.width <= 1) { ctx.fillRect(d.x - 0.5, d.y, 1, d.height); } else { ctx.fillRect(d.x + 0.5, d.y + 0.5, d.width, d.height); if (stroke) { ctx.strokeRect(d.x, d.y, d.width, d.height); } } }); }); }; export function getBars( props: StackedBarSeriesProps, xAccessor: any, yAccessor: any, xScale: any, yScale: any, plotData: any[], stack = identityStack, after = identity, ) { const { baseAt, fillStyle, stroke, spaceBetweenBar = 0 } = props; const getFill = functor(fillStyle); const getBase = functor(baseAt); const widthFunctor = functor(props.width); const width = widthFunctor(props, { xScale, xAccessor, plotData, }); const barWidth = Math.round(width); const eachBarWidth = (barWidth - spaceBetweenBar * (yAccessor.length - 1)) / yAccessor.length; const offset = barWidth === 1 ? 0 : 0.5 * width; const ds = plotData.map((each) => { const d = { appearance: {}, x: xAccessor(each), }; yAccessor.forEach((eachYAccessor: any, i: number) => { const key = `y${i}`; // @ts-ignore d[key] = eachYAccessor(each); const appearance = { stroke: stroke ? getFill(each, i) : "none", fillStyle: getFill(each, i), }; // @ts-ignore d.appearance[key] = appearance; }); return d; }); const keys = yAccessor.map((_: any, i: number) => `y${i}`); // @ts-ignore const data = stack().keys(keys)(ds); const newData = data.map((each: any, i: number) => { const key = each.key; return each.map((d: any) => { const array = [d[0], d[1]]; // @ts-ignore array.data = { x: d.data.x, i, appearance: d.data.appearance[key], }; return array; }); }); const bars = merge(newData) .map((d) => { let y = yScale(d[1]); let h = getBase(xScale, yScale, d.data) - yScale(d[1] - d[0]); if (h < 0) { y = y + h; h = -h; } return { ...d.data.appearance, x: Math.round(xScale(d.data.x) - width / 2), y, groupOffset: Math.round(offset - (d.data.i > 0 ? (eachBarWidth + spaceBetweenBar) * d.data.i : 0)), groupWidth: Math.round(eachBarWidth), offset: Math.round(offset), height: h, width: barWidth, }; }) .filter((bar) => !isNaN(bar.y)); return after(bars); }