import * as d3 from 'd3' import { combineLatest, map, switchMap, takeUntil, distinctUntilChanged, shareReplay, Observable, Subject } from 'rxjs' import type { BasePluginFn } from './types' import type { ComputedDatumGrid, ComputedDataGrid, ComputedLayoutDatumGrid, ComputedAxesDataGrid, DataFormatterTypeMap, EventGrid, ChartParams, ContainerPositionScaled, Layout, TransformData } from '../../lib/core-types' import type { BaseBarsTriangleParams } from '../../lib/plugins-basic-types' import { getD3TransitionEase } from '../utils/d3Utils' import { getClassName, getUniID } from '../utils/orbchartsUtils' import { gridSelectionsObservable } from '../grid/gridObservables' // export interface BaseBarsTriangleParams { // barWidth: number // barPadding: number // barGroupPadding: number // 群組和群組間的間隔 // linearGradientOpacity: [number, number] // } interface BaseBarsContext { selection: d3.Selection computedData$: Observable computedAxesData$: Observable visibleComputedData$: Observable visibleComputedAxesData$: Observable fullDataFormatter$: Observable> seriesLabels$: Observable SeriesDataMap$: Observable> GroupDataMap$: Observable> fullParams$: Observable fullChartParams$: Observable gridAxesTransform$: Observable gridGraphicTransform$: Observable gridAxesSize$: Observable<{ width: number; height: number; }> gridHighlight$: Observable gridContainerPosition$: Observable isSeriesSeprate$: Observable event$: Subject } interface RenderBarParams { graphicGSelection: d3.Selection pathGClassName: string pathClassName: string visibleComputedAxesData: ComputedAxesDataGrid linearGradientIds: string[] zeroYArr: number[] groupLabels: string[] barScale: d3.ScalePoint params: BaseBarsTriangleParams chartParams: ChartParams barWidth: number delayGroup: number transitionItem: number isSeriesSeprate: boolean } // interface BarDatumGrid extends ComputedDatumGrid { // linearGradientId: string // } type ClipPathDatum = { id: string; // x: number; // y: number; width: number; height: number; } // const pluginName = 'BaseBarsTriangle' // const pathGClassName = getClassName(pluginName, 'pathG') // const pathClassName = getClassName(pluginName, 'path') // group的delay在動畫中的佔比(剩餘部份的時間為圖形本身的動畫時間,因為delay時間和最後一個group的動畫時間加總為1) const groupDelayProportionOfDuration = 0.3 function calcBarWidth ({ axisWidth, groupAmount, barAmountOfGroup, barPadding = 0, barGroupPadding = 0 }: { axisWidth: number groupAmount: number barAmountOfGroup: number barPadding: number barGroupPadding: number }) { const eachGroupWidth = groupAmount > 1 // 等於 1 時會算出 Infinity ? axisWidth / (groupAmount - 1) : axisWidth const width = (eachGroupWidth - barGroupPadding) / barAmountOfGroup - barPadding return width > 1 ? width : 1 } function makeBarScale (barWidth: number, seriesLabels: string[], params: BaseBarsTriangleParams) { const barHalfWidth = barWidth! / 2 const barGroupWidth = barWidth * seriesLabels.length + params.barPadding! * seriesLabels.length return d3.scalePoint() .domain(seriesLabels) .range([-barGroupWidth / 2 + barHalfWidth, barGroupWidth / 2 - barHalfWidth]) } function calcDelayGroup (barGroupAmount: number, totalDuration: number) { if (barGroupAmount <= 1) { // 一筆內計算會出錯所以不算 return 0 } return totalDuration / (barGroupAmount - 1) * groupDelayProportionOfDuration // 依group數量計算 } function calctransitionItem (barGroupAmount: number, totalDuration: number) { if (barGroupAmount <= 1) { // 一筆內不會有delay return totalDuration } return totalDuration * (1 - groupDelayProportionOfDuration) // delay後剩餘的時間 } function renderTriangleBars ({ graphicGSelection, pathGClassName, pathClassName, visibleComputedAxesData, linearGradientIds, zeroYArr, groupLabels, barScale, params, chartParams, barWidth, delayGroup, transitionItem, isSeriesSeprate }: RenderBarParams) { const barHalfWidth = barWidth! / 2 graphicGSelection .each((d, seriesIndex, g) => { // g const gSelection = d3.select(g[seriesIndex]) .selectAll(`g.${pathGClassName}`) .data(visibleComputedAxesData[seriesIndex] ?? []) .join( enter => { const enterSelection = enter .append('g') .classed(pathGClassName, true) .attr('cursor', 'pointer') enterSelection .append('path') .classed(pathClassName, true) .style('vector-effect', 'non-scaling-stroke') .attr('d', (d) => { const x = -barHalfWidth const y1 = zeroYArr[seriesIndex] const y2 = zeroYArr[seriesIndex] return `M${x},${y1} L${x + (barWidth! / 2)},${y2} ${x + barWidth!},${y1}` }) return enterSelection }, update => update, exit => exit.remove() ) .attr('transform', d => `translate(${isSeriesSeprate ? 0 : barScale(d.seriesLabel)!}, 0)`) // path gSelection.select(`path.${pathClassName}`) .attr('height', d => Math.abs(d.axisYFromZero) || 1) // 無值還是給一個 1 的高度 .attr('y', d => d.axisY < zeroYArr[seriesIndex] ? d.axisY : zeroYArr[seriesIndex]) .attr('x', d => isSeriesSeprate ? 0 : barScale(d.seriesLabel)!) // .style('fill', d => `url(#${d.linearGradientId})`) .style('fill', d => `url(#${linearGradientIds[d.seriesIndex]})`) .attr('stroke', d => d.color) .attr('transform', d => `translate(${(d ? d.axisX : 0)}, ${0})`) .transition() .duration(transitionItem) .ease(getD3TransitionEase(chartParams.transitionEase)) .delay((d, i) => d.groupIndex * delayGroup) // .attr('transform', `translate(${-barHalfWidth}, 0)`) // .attr('x', d => itemScale(d.itemLabel)!) // .attr('y', d => -d.y) .attr('d', (d) => { const x = -barHalfWidth const y1 = zeroYArr[seriesIndex] const y2 = d.axisY return `M${x},${y1} L${x + (barWidth! / 2)},${y2} ${x + barWidth!},${y1}` }) }) const graphicBarSelection: d3.Selection = graphicGSelection.selectAll(`path.${pathClassName}`) return graphicBarSelection } function renderLinearGradient ({ defsSelection, computedData, linearGradientIds, params }: { defsSelection: d3.Selection computedData: ComputedDataGrid linearGradientIds: string[] params: BaseBarsTriangleParams }) { defsSelection! .selectAll('linearGradient') .data(computedData ?? []) .join( enter => { return enter .append('linearGradient') .attr('x1', '0%') .attr('x2', '0%') .attr('y1', '100%') .attr('y2', '0%') .attr('spreadMethod', 'pad') }, update => update, exit => exit.remove() ) .attr('id', (d, i) => { return d[0] ? linearGradientIds[d[0].seriesIndex] : '' }) .html((d, i) => { const color = d[0] ? d[0].color : '' return ` ` }) } function renderClipPath ({ defsSelection, clipPathData }: { defsSelection: d3.Selection clipPathData: ClipPathDatum[] }) { const clipPath = defsSelection .selectAll('clipPath') .data(clipPathData) .join( enter => { return enter .append('clipPath') }, update => update, exit => exit.remove() ) .attr('id', d => d.id) .each((d, i, g) => { const rect = d3.select(g[i]) .selectAll('rect') .data([d]) .join( enter => { return enter .append('rect') }, update => update, exit => exit.remove() ) .attr('x', 0) .attr('y', 0) .attr('width', _d => _d.width) .attr('height', _d => _d.height) }) } function highlight ({ selection, ids, fullChartParams }: { selection: d3.Selection ids: string[] fullChartParams: ChartParams }) { selection.interrupt('highlight') const removeHighlight = () => { selection .transition('highlight') .duration(200) .style('opacity', 1) } if (!ids.length) { removeHighlight() return } selection .each((d, i, n) => { if (ids.includes(d.id)) { d3.select(n[i]) .style('opacity', 1) } else { d3.select(n[i]) .style('opacity', fullChartParams.styles.unhighlightedOpacity) } }) } export const createBaseBarsTriangle: BasePluginFn = (pluginName: string, { selection, computedData$, computedAxesData$, visibleComputedData$, visibleComputedAxesData$, fullDataFormatter$, seriesLabels$, SeriesDataMap$, GroupDataMap$, fullParams$, fullChartParams$, gridAxesTransform$, gridGraphicTransform$, gridAxesSize$, gridHighlight$, gridContainerPosition$, isSeriesSeprate$, event$ }) => { const destroy$ = new Subject() const clipPathID = getUniID(pluginName, 'clipPath-box') const pathGClassName = getClassName(pluginName, 'pathG') const pathClassName = getClassName(pluginName, 'path') const { seriesSelection$, axesSelection$, defsSelection$, graphicGSelection$ } = gridSelectionsObservable({ selection, pluginName, clipPathID, seriesLabels$, gridContainerPosition$, gridAxesTransform$, gridGraphicTransform$ }) // valueAxis 的起始座標 const valueAxisStart$: Observable = gridGraphicTransform$.pipe( takeUntil(destroy$), map(data => { // 抵消掉外層的變型 return - data.translate[1] / data.scale[1] }) ) const zeroYArr$ = visibleComputedAxesData$.pipe( // map(d => d[0] && d[0][0] // ? d[0][0].axisY - d[0][0].axisYFromZero // : 0), map(data => { return data.map(d => { return d[0] ? d[0].axisY - d[0].axisYFromZero : 0 }) }), distinctUntilChanged() ) const barWidth$ = combineLatest({ computedData: computedData$, visibleComputedData: visibleComputedData$, params: fullParams$, gridAxesSize: gridAxesSize$, isSeriesSeprate: isSeriesSeprate$ }).pipe( takeUntil(destroy$), switchMap(async d => d), map(data => { if (data.params.barWidth) { return data.params.barWidth } else if (data.isSeriesSeprate) { return calcBarWidth({ axisWidth: data.gridAxesSize.width, groupAmount: data.computedData[0] ? data.computedData[0].length : 0, barAmountOfGroup: 1, barPadding: data.params.barPadding, barGroupPadding: data.params.barGroupPadding }) } else { return calcBarWidth({ axisWidth: data.gridAxesSize.width, groupAmount: data.computedData[0] ? data.computedData[0].length : 0, barAmountOfGroup: data.visibleComputedData.length, barPadding: data.params.barPadding, barGroupPadding: data.params.barGroupPadding }) } }) ) // const seriesLabels$ = visibleComputedData$.pipe( // takeUntil(destroy$), // map(data => { // const SeriesLabelSet: Set = new Set() // data.forEach(d => { // d.forEach(_d => { // SeriesLabelSet.add(_d.seriesLabel) // }) // }) // return Array.from(SeriesLabelSet) // }) // ) const groupLabels$ = visibleComputedData$.pipe( takeUntil(destroy$), map(data => { const GroupLabelSet: Set = new Set() data.forEach(d => { d.forEach(_d => { GroupLabelSet.add(_d.groupLabel) }) }) return Array.from(GroupLabelSet) }) ) const barScale$: Observable> = new Observable(subscriber => { combineLatest({ seriesLabels: seriesLabels$, barWidth: barWidth$, params: fullParams$, }).pipe( takeUntil(destroy$), switchMap(async d => d) ).subscribe(data => { const barScale = makeBarScale(data.barWidth, data.seriesLabels, data.params) subscriber.next(barScale) }) }) const transitionDuration$ = fullChartParams$.pipe( takeUntil(destroy$), map(d => d.transitionDuration), distinctUntilChanged() ) const delayGroup$ = new Observable(subscriber => { combineLatest({ groupLabels: groupLabels$, transitionDuration: transitionDuration$, }).pipe( switchMap(async d => d) ).subscribe(data => { const delay = calcDelayGroup(data.groupLabels.length, data.transitionDuration) subscriber.next(delay) }) }).pipe( takeUntil(destroy$), distinctUntilChanged() ) const transitionItem$ = new Observable(subscriber => { combineLatest({ groupLabels: groupLabels$, transitionDuration: transitionDuration$ }).pipe( switchMap(async d => d) ).subscribe(data => { const transition = calctransitionItem(data.groupLabels.length, data.transitionDuration) subscriber.next(transition) }) }).pipe( takeUntil(destroy$), distinctUntilChanged() ) // combineLatest({ defsSelection: defsSelection$, gridAxesSize: gridAxesSize$, }).pipe( takeUntil(destroy$), switchMap(async d => d) ).subscribe(data => { const clipPathData = [{ id: clipPathID, width: data.gridAxesSize.width, height: data.gridAxesSize.height }] renderClipPath({ defsSelection: data.defsSelection, clipPathData }) }) const highlightTarget$ = fullChartParams$.pipe( takeUntil(destroy$), map(d => d.highlightTarget), distinctUntilChanged() ) const linearGradientIds$ = seriesLabels$.pipe( takeUntil(destroy$), map(d => d.map((d, i) => { return getUniID(pluginName, `lineargradient-${d}`) })) ) // const barData$ = combineLatest({ // linearGradientIds: linearGradientIds$, // computedData: computedData$ // }).pipe( // takeUntil(destroy$), // switchMap(async d => d), // map(data => { // return data.computedData.map((series, seriesIndex) => { // return series.map((_d, _i) => { // return { // linearGradientId: data.linearGradientIds[seriesIndex], // ..._d // } // }) // }) // }) // ) const barSelection$ = combineLatest({ graphicGSelection: graphicGSelection$, defsSelection: defsSelection$, computedData: computedData$, visibleComputedAxesData: visibleComputedAxesData$, linearGradientIds: linearGradientIds$, zeroYArr: zeroYArr$, groupLabels: groupLabels$, barScale: barScale$, params: fullParams$, chartParams: fullChartParams$, barWidth: barWidth$, delayGroup: delayGroup$, transitionItem: transitionItem$, isSeriesSeprate: isSeriesSeprate$ }).pipe( takeUntil(destroy$), switchMap(async (d) => d), map(data => { renderLinearGradient({ defsSelection: data.defsSelection, computedData: data.computedData, linearGradientIds: data.linearGradientIds, params: data.params }) return renderTriangleBars({ graphicGSelection: data.graphicGSelection, pathGClassName, pathClassName, visibleComputedAxesData: data.visibleComputedAxesData, linearGradientIds: data.linearGradientIds, zeroYArr: data.zeroYArr, groupLabels: data.groupLabels, barScale: data.barScale, params: data.params, chartParams: data.chartParams, barWidth: data.barWidth, delayGroup: data.delayGroup, transitionItem: data.transitionItem, isSeriesSeprate: data.isSeriesSeprate }) }) ) combineLatest({ barSelection: barSelection$, computedData: computedData$, highlightTarget: highlightTarget$, SeriesDataMap: SeriesDataMap$, GroupDataMap: GroupDataMap$, }).subscribe(data => { data.barSelection! .on('mouseover', (event, datum) => { event.stopPropagation() event$.next({ type: 'grid', eventName: 'mouseover', pluginName, highlightTarget: data.highlightTarget, datum, gridIndex: datum.gridIndex, series: data.SeriesDataMap.get(datum.seriesLabel)!, seriesIndex: datum.seriesIndex, seriesLabel: datum.seriesLabel, group: data.GroupDataMap.get(datum.groupLabel)!, groupIndex: datum.groupIndex, groupLabel: datum.groupLabel, event, data: data.computedData }) }) .on('mousemove', (event, datum) => { event.stopPropagation() event$.next({ type: 'grid', eventName: 'mousemove', pluginName, highlightTarget: data.highlightTarget, datum, gridIndex: datum.gridIndex, series: data.SeriesDataMap.get(datum.seriesLabel)!, seriesIndex: datum.seriesIndex, seriesLabel: datum.seriesLabel, group: data.GroupDataMap.get(datum.groupLabel)!, groupIndex: datum.groupIndex, groupLabel: datum.groupLabel, event, data: data.computedData }) }) .on('mouseout', (event, datum) => { event.stopPropagation() event$.next({ type: 'grid', eventName: 'mouseout', pluginName, highlightTarget: data.highlightTarget, datum, gridIndex: datum.gridIndex, series: data.SeriesDataMap.get(datum.seriesLabel)!, seriesIndex: datum.seriesIndex, seriesLabel: datum.seriesLabel, group: data.GroupDataMap.get(datum.groupLabel)!, groupIndex: datum.groupIndex, groupLabel: datum.groupLabel, event, data: data.computedData }) }) .on('click', (event, datum) => { event.stopPropagation() event$.next({ type: 'grid', eventName: 'click', pluginName, highlightTarget: data.highlightTarget, datum, gridIndex: datum.gridIndex, series: data.SeriesDataMap.get(datum.seriesLabel)!, seriesIndex: datum.seriesIndex, seriesLabel: datum.seriesLabel, group: data.GroupDataMap.get(datum.groupLabel)!, groupIndex: datum.groupIndex, groupLabel: datum.groupLabel, event, data: data.computedData }) }) }) combineLatest({ barSelection: barSelection$, highlight: gridHighlight$.pipe( map(data => data.map(d => d.id)) ), fullChartParams: fullChartParams$ }).pipe( takeUntil(destroy$), switchMap(async d => d) ).subscribe(data => { highlight({ selection: data.barSelection, ids: data.highlight, fullChartParams: data.fullChartParams }) }) return () => { destroy$.next(undefined) } }