import * as d3 from 'd3' import { Observable, Subject, combineLatest, takeUntil, map, distinctUntilChanged, of, iif, switchMap, shareReplay } from 'rxjs' import type { ChartParams, ComputedDatumMultiValue, ComputedDataMultiValue, ComputedXYDatumMultiValue, ContainerSize, ContainerPositionScaled, DataFormatterMultiValue, Layout } from '../../lib/core-types' import type { BaseRacingValueLabelsParams } from '../../lib/plugins-basic-types' import type { BasePluginFn } from './types' import { getColor, getClassName, getUniID } from '../utils/orbchartsUtils' import { parseTickFormatValue } from '../utils/d3Utils' import { multiValueContainerSelectionsObservable } from '../multiValue/multiValueObservables' interface BaseRacingAxisContext { selection: d3.Selection computedData$: Observable visibleComputedRankingData$: Observable rankingScaleList$: Observable>> xScale$: Observable<(n: number) => number> computedRankingAmount$: Observable fullParams$: Observable // fullDataFormatter$: Observable fullChartParams$: Observable // layout$: Observable containerPosition$: Observable containerSize$: Observable isCategorySeprate$: Observable // xyValueIndex$: Observable<[number, number]> } type ClipPathDatum = { id: string; // x: number; // y: number; width: number; height: number; } // const pluginName = 'RacingAxis' const yTickTextAnchor = 'start' const yTickDominantBaseline = 'middle' function renderRacingLabels ({ selection, fullParams, fullChartParams, rankingScale, xScale, categoryData, computedRankingAmount }: { selection: d3.Selection, fullParams: BaseRacingValueLabelsParams fullChartParams: ChartParams rankingScale: d3.ScalePoint xScale: ((n: number) => number) categoryData: ComputedDatumMultiValue[] computedRankingAmount: number // textReverseTransform: string // xyValueIndex: [number, number] }) { const labelData = categoryData.slice(0, computedRankingAmount + 1) // 將多餘的label移除避免不避要的計算 const labelGSelection = selection .selectAll(`g`) .data(labelData, (d: ComputedDatumMultiValue) => d.id) // .join('g') // .classed(boxClassName, true) .join( enter => { return enter .append('g') // .attr('x', d => xScale(d.value[xyValueIndex[0]]) + fullParams.padding) // .attr('y', d => rankingScale(d.label)!) .attr('transform', d => `translate(${xScale((d.value[d.xValueIndex] ?? 0)) + fullParams.padding}, ${rankingScale(d.label)!})`) }, update => { return update .transition() .duration(fullChartParams.transitionDuration) .ease(d3.easeLinear) // // 偏移使用 x, y 而非 transform 才不會受到外層 scale 變形影響 // .attr('x', d => xScale(d.value[xyValueIndex[0]]) + fullParams.padding) // .attr('y', d => rankingScale(d.label)!) .attr('transform', d => `translate(${xScale((d.value[d.xValueIndex] ?? 0)) + fullParams.padding}, ${rankingScale(d.label)!})`) }, exit => exit.remove() ) .each((d, i, g) => { const gSelection = d3.select(g[i]) gSelection .selectAll('text') .data([d]) .join( enter => { return enter .append('text') }, update => update, exit => exit.remove() ) .attr('text-anchor', yTickTextAnchor) .attr('dominant-baseline', yTickDominantBaseline) .attr('font-size', fullChartParams.styles.textSize) .style('fill', getColor(fullParams.colorType, fullChartParams)) // .style('transform', textReverseTransform) // .style('transform', textReverseTransformWithRotate) // .text(d => parseTickFormatValue(d.value[xyValueIndex[0]], fullParams.format)) .transition() .duration(fullChartParams.transitionDuration) .ease(d3.easeLinear) .textTween(datum => { const currentIndex = datum.xValueIndex const prevIndex = currentIndex === 0 ? 0 : currentIndex - 1 const prev = datum.value[prevIndex] const current = datum.value[currentIndex] return t => { const tweenNumber = t === 1 ? d3.interpolateNumber(prev, current)(t) : d3.interpolateRound(prev, current)(t) // 過渡時使用整數避免小數位數爆炸多 return parseTickFormatValue(tweenNumber, fullParams.format) } }) }) // const labelSelection = selection // .selectAll(`text`) // .data(categoryData, (d: ComputedDatumMultiValue) => d.id) // // .join('g') // // .classed(boxClassName, true) // .join( // enter => { // return enter // .append('text') // // .style('font-weight', 'bold') // .attr('x', d => xScale(d.value[xyValueIndex[0]]) + fullParams.padding) // .attr('y', d => rankingScale(d.label)!) // }, // update => { // return update // .transition() // .duration(fullChartParams.transitionDuration) // .ease(d3.easeLinear) // // 偏移使用 x, y 而非 transform 才不會受到外層 scale 變形影響 // .attr('x', d => xScale(d.value[xyValueIndex[0]]) + fullParams.padding) // .attr('y', d => rankingScale(d.label)!) // }, // exit => exit.remove() // ) // .attr('text-anchor', yTickTextAnchor) // .attr('dominant-baseline', yTickDominantBaseline) // .attr('font-size', fullChartParams.styles.textSize) // .style('fill', getColor(fullParams.colorType, fullChartParams)) // // .style('transform', textReverseTransformWithRotate) // // .text(d => parseTickFormatValue(d.value[xyValueIndex[0]], fullParams.format)) // .transition() // .duration(fullChartParams.transitionDuration) // .ease(d3.easeLinear) // .textTween(datum => { // const currentIndex = xyValueIndex[0] // const prevIndex = currentIndex === 0 ? 0 : currentIndex - 1 // const prev = datum.value[prevIndex] // const current = datum.value[currentIndex] // return t => { // const tweenNumber = d3.interpolateNumber(prev, current)(t) // return parseTickFormatValue(tweenNumber, fullParams.format) // } // }) const labelSelection: d3.Selection = labelGSelection.selectAll(`text`) return labelSelection } function renderClipPath ({ defsSelection, clipPathData }: { defsSelection: d3.Selection clipPathData: ClipPathDatum[] // textReverseTransform: string }) { const clipPath = defsSelection .selectAll('clipPath') .data(clipPathData) .join( enter => { return enter .append('clipPath') }, update => update, exit => exit.remove() ) .attr('id', d => d.id) // .attr('transform', textReverseTransform) .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', _d => 0) .attr('y', 0) .attr('width', _d => _d.width * 1.5) // value太長時右側可能會超出區塊 .attr('height', _d => _d.height) }) } export const createBaseRacingValueLabels: BasePluginFn = (pluginName: string, { selection, computedData$, visibleComputedRankingData$, rankingScaleList$, xScale$, computedRankingAmount$, fullParams$, // fullDataFormatter$, fullChartParams$, // layout$, containerPosition$, containerSize$, isCategorySeprate$, // xyValueIndex$ }) => { const destroy$ = new Subject() const containerClassName = getClassName(pluginName, 'container') const boxClassName = getClassName(pluginName, 'box') const clipPathID = getUniID(pluginName, 'clipPath-box') const containerSelection$ = multiValueContainerSelectionsObservable({ selection, pluginName, clipPathID: null, computedData$, containerPosition$, isCategorySeprate$, }).pipe( takeUntil(destroy$), ) // const containerSelection$ = combineLatest({ // computedData: computedData$.pipe( // distinctUntilChanged((a, b) => { // // 只有當series的數量改變時,才重新計算 // return a.length === b.length // }), // ), // isCategorySeprate: isCategorySeprate$ // }).pipe( // takeUntil(destroy$), // switchMap(async (d) => d), // map(data => { // return data.isCategorySeprate // // category分開的時候顯示各別axis // ? data.computedData // // category合併的時候只顯示第一個axis // : [data.computedData[0]] // }), // map((computedData, i) => { // return selection // .selectAll(`g.${containerClassName}`) // .data(computedData, d => d[0] ? d[0].categoryIndex : i) // .join('g') // .classed(containerClassName, true) // }) // ) // combineLatest({ // containerSelection: containerSelection$, // containerPosition: containerPosition$ // }).pipe( // takeUntil(destroy$), // switchMap(async d => d) // ).subscribe(data => { // data.containerSelection // .attr('transform', (d, i) => { // const containerPosition = data.containerPosition[i] ?? data.containerPosition[0] // const translate = containerPosition.translate // const scale = containerPosition.scale // // return `translate(${translate[0]}, ${translate[1]}) scale(${scale[0]}, ${scale[1]})` // return `translate(${translate[0]}, ${translate[1]})` // }) // // .attr('opacity', 0) // // .transition() // // .attr('opacity', 1) // }) // const textReverseTransform$ = containerPosition$.pipe( // takeUntil(destroy$), // switchMap(async (d) => d), // map(containerPosition => { // // const axesRotateXYReverseValue = `rotateX(${data.gridAxesReverseTransform.rotateX}deg) rotateY(${data.gridAxesReverseTransform.rotateY}deg)` // // const axesRotateReverseValue = `rotate(${data.gridAxesReverseTransform.rotate}deg)` // const containerScaleReverseValue = `scale(${1 / containerPosition[0].scale[0]}, ${1 / containerPosition[0].scale[1]})` // // 抵消最外層scale // return `${containerScaleReverseValue}` // }), // distinctUntilChanged() // ) // const textReverseTransformWithRotate$ = combineLatest({ // textReverseTransform: textReverseTransform$, // fullParams: fullParams$, // }).pipe( // takeUntil(destroy$), // switchMap(async (d) => d), // map(data => { // // 必須按照順序(先抵消外層rotate,再抵消最外層scale,最後再做本身的rotate) // return `${data.textReverseTransform} rotate(${data.fullParams.barLabel.rotate}deg)` // }) // ) // const rankingLabelList$ = visibleComputedRankingData$.pipe( // takeUntil(destroy$), // map(data => { // return data.map(categoryData => categoryData.map(d => d.label)) // }) // ) containerSize$.subscribe(data => { const defsSelection = selection.selectAll('defs') .data([clipPathID]) .join('defs') const clipPathData = [{ id: clipPathID, width: data.width, height: data.height }] renderClipPath({ defsSelection: defsSelection, clipPathData, // textReverseTransform: data.textReverseTransform }) }) combineLatest({ containerSelection: containerSelection$, fullParams: fullParams$, fullChartParams: fullChartParams$, visibleComputedRankingData: visibleComputedRankingData$, rankingScaleList: rankingScaleList$, xScale: xScale$, computedRankingAmount: computedRankingAmount$, // textReverseTransform: textReverseTransform$, // xyValueIndex: xyValueIndex$ }).pipe( takeUntil(destroy$), switchMap(async (d) => d), ).subscribe(data => { data.containerSelection.each((d, i, g) => { const _containerSelection = d3.select(g[i]) // const rankingLabels = data.rankingLabelList[i] const rankingScale = data.rankingScaleList[i] if (!rankingScale) { return } // const containerClipPathID = `${clipPathID}-${i}` const axisSelection = _containerSelection .selectAll(`g.${boxClassName}`) .data([i]) .join('g') .attr('class', boxClassName) .attr('clip-path', `url(#${clipPathID})`) renderRacingLabels({ selection: axisSelection, fullParams: data.fullParams, fullChartParams: data.fullChartParams, rankingScale: rankingScale, categoryData: data.visibleComputedRankingData[i], // textReverseTransform: data.textReverseTransform, // textReverseTransformWithRotate: data.textReverseTransformWithRotate, xScale: data.xScale, computedRankingAmount: data.computedRankingAmount, // xyValueIndex: data.xyValueIndex, }) }) }) return () => { destroy$.next(undefined) } }