import * as d3 from 'd3' import { combineLatest, map, switchMap, takeUntil, distinctUntilChanged, Observable, Subject } from 'rxjs' import type { BasePluginFn } from './types' import type { ComputedDatumGrid, ComputedDataGrid, ComputedAxesDataGrid, EventGrid, ChartParams, ContainerPositionScaled, Layout, TransformData, ColorType } from '../../lib/core-types' import type { BaseDotsParams } from '../../lib/plugins-basic-types' import { getDatumColor, getClassName, getUniID } from '../utils/orbchartsUtils' import { gridSelectionsObservable } from '../grid/gridObservables' // export interface BaseDotsParams { // radius: number // fillColorType: ColorType // strokeColorType: ColorType // strokeWidth: number // // strokeWidthWhileHighlight: number // onlyShowHighlighted: boolean // } interface BaseDotsContext { selection: d3.Selection computedData$: Observable computedAxesData$: Observable visibleComputedData$: Observable visibleComputedAxesData$: Observable seriesLabels$: Observable SeriesDataMap$: Observable> GroupDataMap$: Observable> fullParams$: Observable fullChartParams$: Observable gridAxesTransform$: Observable gridGraphicTransform$: Observable gridGraphicReverseScale$: Observable<[number, number][]> gridAxesSize$: Observable<{ width: number; height: number; }> gridHighlight$: Observable gridContainerPosition$: Observable event$: Subject } type ClipPathDatum = { id: string; // x: number; // y: number; width: number; height: number; } // const pluginName = 'Dots' // const circleGClassName = getClassName(pluginName, 'circleG') // const circleClassName = getClassName(pluginName, 'circle') function renderDots ({ graphicGSelection, circleGClassName, circleClassName, visibleComputedAxesData, fullParams, fullChartParams, graphicReverseScale }: { graphicGSelection: d3.Selection circleGClassName: string circleClassName: string visibleComputedAxesData: ComputedAxesDataGrid fullParams: BaseDotsParams fullChartParams: ChartParams graphicReverseScale: [number, number][] }) { const createEnterDuration = (enter: d3.Selection) => { const enterSize = enter.size() const eachDuration = fullChartParams.transitionDuration / enterSize return eachDuration } // enterDuration let enterDuration = 0 graphicGSelection .each((seriesData, seriesIndex, g) => { d3.select(g[seriesIndex]) .selectAll('g') .data(visibleComputedAxesData[seriesIndex], d => d.id) .join( enter => { // enterDuration enterDuration = createEnterDuration(enter) return enter .append('g') .classed(circleGClassName, true) }, update => update, exit => exit.remove() ) .attr('transform', d => `translate(${d.axisX}, ${d.axisY})`) .each((d, i, g) => { const circle = d3.select(g[i]) .selectAll('circle') .data([d]) .join( enter => { return enter .append('circle') .style('cursor', 'pointer') .style('vector-effect', 'non-scaling-stroke') .classed(circleClassName, true) .attr('opacity', 0) .transition() .delay((_d, _i) => { return i * enterDuration }) .attr('opacity', 1) }, update => { return update .transition() .duration(50) // .attr('cx', d => d.axisX) // .attr('cy', d => d.axisY) .attr('opacity', 1) }, exit => exit.remove() ) .attr('r', fullParams.radius) .attr('fill', (d, i) => getDatumColor({ datum: d, colorType: fullParams.fillColorType, fullChartParams })) .attr('stroke', (d, i) => getDatumColor({ datum: d, colorType: fullParams.strokeColorType, fullChartParams })) .attr('stroke-width', fullParams.strokeWidth) .attr('transform', `scale(${graphicReverseScale[seriesIndex][0] ?? 1}, ${graphicReverseScale[seriesIndex][1] ?? 1})`) }) }) const graphicCircleSelection: d3.Selection = graphicGSelection.selectAll(`circle.${circleClassName}`) return graphicCircleSelection } function highlightDots ({ selection, ids, onlyShowHighlighted, fullChartParams }: { selection: d3.Selection ids: string[] onlyShowHighlighted: boolean // fullParams: BaseDotsParams fullChartParams: ChartParams }) { selection.interrupt('highlight') if (!ids.length) { // remove highlight selection .transition('highlight') .duration(200) .style('opacity', onlyShowHighlighted === true ? 0 : 1) // selection // .attr('stroke-width', fullParams.strokeWidth) return } selection .each((d, i, n) => { if (ids.includes(d.id)) { const dot = d3.select(n[i]) dot .style('opacity', 1) .transition('highlight') .duration(200) // dot // .attr('stroke-width', fullParams.strokeWidthWhileHighlight) } else { const dot = d3.select(n[i]) dot .style('opacity', onlyShowHighlighted === true ? 0 : fullChartParams.styles.unhighlightedOpacity) .transition('highlight') .duration(200) // dot // .attr('stroke-width', fullParams.strokeWidth) } }) } 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('rect') .attr('x', 0) .attr('y', 0) .attr('width', _d => _d.width) .attr('height', _d => _d.height) }) } export const createBaseDots: BasePluginFn = (pluginName: string, { selection, computedData$, computedAxesData$, visibleComputedData$, visibleComputedAxesData$, seriesLabels$, SeriesDataMap$, GroupDataMap$, fullParams$, fullChartParams$, gridAxesTransform$, gridGraphicTransform$, gridGraphicReverseScale$, gridAxesSize$, gridHighlight$, gridContainerPosition$, event$ }) => { const destroy$ = new Subject() const clipPathID = getUniID(pluginName, 'clipPath-box') const circleGClassName = getClassName(pluginName, 'circleG') const circleClassName = getClassName(pluginName, 'circle') // const axisSelection: d3.Selection = selection // .append('g') // .attr('clip-path', `url(#${clipPathID})`) // const defsSelection: d3.Selection = axisSelection.append('defs') // const dataAreaSelection: d3.Selection = axisSelection.append('g') // const graphicSelection$: Subject> = new Subject() const { seriesSelection$, axesSelection$, defsSelection$, graphicGSelection$ } = gridSelectionsObservable({ selection, pluginName, clipPathID, seriesLabels$, gridContainerPosition$, gridAxesTransform$, gridGraphicTransform$ }) const graphicReverseScale$: Observable<[number, number][]> = combineLatest({ // gridGraphicTransform: gridGraphicTransform$, // gridContainerPosition: gridContainerPosition$, // gridAxesTransform: gridAxesTransform$ computedData: computedData$, gridGraphicReverseScale: gridGraphicReverseScale$ }).pipe( takeUntil(destroy$), switchMap(async data => data), map(data => { return data.computedData.map((series, seriesIndex) => { return data.gridGraphicReverseScale[seriesIndex] }) }) ) const clipPathSubscription = 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 graphicSelection$ = combineLatest({ graphicGSelection: graphicGSelection$, visibleComputedAxesData: visibleComputedAxesData$, graphicReverseScale: graphicReverseScale$, fullChartParams: fullChartParams$, fullParams: fullParams$, }).pipe( takeUntil(destroy$), switchMap(async (d) => d), map(data => { return renderDots({ graphicGSelection: data.graphicGSelection, circleGClassName, circleClassName, visibleComputedAxesData: data.visibleComputedAxesData, fullParams: data.fullParams, fullChartParams: data.fullChartParams, graphicReverseScale: data.graphicReverseScale }) }) ) combineLatest({ graphicSelection: graphicSelection$, computedData: computedData$, SeriesDataMap: SeriesDataMap$, GroupDataMap: GroupDataMap$, highlightTarget: highlightTarget$ }).pipe( takeUntil(destroy$), switchMap(async (d) => d), ).subscribe(data => { data.graphicSelection .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, data: data.computedData, 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 }) }) .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 }) }) }) // const datumList$ = computedData$.pipe( // takeUntil(destroy$), // map(d => d.flat()) // ) // const highlight$ = highlightObservable({ datumList$, fullChartParams$, event$: store.event$ }) // const highlightSubscription = gridHighlight$.subscribe() const onlyShowHighlighted$ = fullParams$.pipe( takeUntil(destroy$), map(d => d.onlyShowHighlighted), distinctUntilChanged() ) combineLatest({ graphicSelection: graphicSelection$, highlight: gridHighlight$.pipe( map(data => data.map(d => d.id)) ), onlyShowHighlighted: onlyShowHighlighted$, // fullParams: fullParams$, fullChartParams: fullChartParams$ }).pipe( takeUntil(destroy$), switchMap(async d => d) ).subscribe(data => { highlightDots({ selection: data.graphicSelection, ids: data.highlight, onlyShowHighlighted: data.onlyShowHighlighted, // fullParams: data.fullParams, fullChartParams: data.fullChartParams }) }) return () => { destroy$.next(undefined) // highlightSubscription.unsubscribe() } }