import type { FormationItem, FormationSettings } from '@/api/formationReports'
|
import type { PersistedTestRecord } from '@/api/tests'
|
import { formationSeriesKey, formationSeriesColor } from './formationCycleChart'
|
import { measurementChartOption, type ReportMetricKey } from './reportMeasurementChart'
|
|
export const formationStepGroups = [{ key: 'charge', title: '充电' }, { key: 'discharge', title: '放电' }] as const
|
export type FormationStepGroup = typeof formationStepGroups[number]['key']
|
export function formationCycleKey(item: FormationItem, cycle: number) { return formationSeriesKey(item) + '|' + cycle }
|
|
/** 当前循环图例事件只更新其中出现的来源,保留其他循环独有电池的显示设置。 */
|
export function formationHiddenSeries(hidden: string[], selected: Record<string, boolean>) {
|
const next = new Set(hidden)
|
for (const [key, shown] of Object.entries(selected)) shown ? next.delete(key) : next.add(key)
|
return [...next]
|
}
|
|
/** 化成只使用可靠识别的实际工步,逻辑合并与人工范围均不参与。 */
|
export function formationChargeSteps(item: FormationItem, cycle: number, _manual?: Record<string, number[]>) {
|
return (item.chargeSteps?.[cycle] ?? []).filter(step => step.direction === 'CHARGE').sort((a,b)=>a.stepNo-b.stepNo)
|
}
|
export function formationSelectedSteps(item: FormationItem, cycle: number, group: FormationStepGroup, _settings?: Pick<FormationSettings, 'curveStepSelections' | 'manualChargeSteps'>) {
|
return (item.chargeSteps?.[cycle] ?? []).filter(step => step.direction === (group === 'charge' ? 'CHARGE' : 'DISCHARGE')).map(step=>step.stepNo)
|
}
|
function emptyPoint(row: PersistedTestRecord): PersistedTestRecord {
|
return {...row, voltage:null,current:null,temperature:null,positivePotential:null,negativePotential:null}
|
}
|
export function formationStepSeries(items: FormationItem[], cycle: number, group: FormationStepGroup, settings: FormationSettings) {
|
return items.filter(item=>item.cycleNos.includes(cycle)).map(item => {
|
const index = items.indexOf(item), key = formationSeriesKey(item)
|
const selected = new Set(formationSelectedSteps(item,cycle,group))
|
const allSteps = [...(item.chargeSteps?.[cycle] ?? [])].sort((a,b)=>a.stepNo-b.stepNo)
|
const all = [...(item.curves?.[cycle]?.records ?? [])].sort((a,b)=>Number(a.deviceTime ?? a.collectorDeviceTime)-Number(b.deviceTime ?? b.collectorDeviceTime))
|
const chosen = all.filter(row=>selected.has(row.stepNo))
|
const times = chosen.flatMap(row=>[row.deviceTime,row.collectorDeviceTime]).filter((time): time is number=>time!=null).map(Number)
|
const zeroTime = times.length ? Math.min(...times) : null
|
const endTime = times.length ? Math.max(...times) : null
|
const records: PersistedTestRecord[] = []
|
let previousStep: number | undefined
|
for (const row of all) {
|
const times = [row.deviceTime,row.collectorDeviceTime].filter(time=>time!=null).map(Number)
|
if (zeroTime==null || endTime==null || !times.some(time=>time>=zeroTime && time<=endTime)) continue
|
if (selected.has(row.stepNo)) {
|
// 即使静置没有实时记录,也用空值绘图分隔符断开两段;不修改冻结记录。
|
if (previousStep!=null && row.stepNo!==previousStep && allSteps.some(step=>step.stepNo>previousStep! && step.stepNo<row.stepNo && !selected.has(step.stepNo))) records.push(emptyPoint(row))
|
records.push(row); previousStep=row.stepNo
|
} else records.push(emptyPoint(row))
|
}
|
return {key,name:item.source.name+'-'+item.source.batteryNo,zeroTime,records,hidden:settings.hiddenSeriesKeys?.includes(key)??false,color:formationSeriesColor(item,index,settings.seriesColors??{})}
|
})
|
}
|
/** 一电池一方向一条曲线,以该方向最早有效设备时间归零,保留实际间隔。 */
|
export function formationStepChartOption(metric: { key: ReportMetricKey; unit: string }, items: FormationItem[], cycle: number, group: FormationStepGroup, settings: FormationSettings) {
|
const series = formationStepSeries(items,cycle,group,settings)
|
const option = measurementChartOption(metric,series,series.map(item=>item.color))
|
// ECharts 按名称控制图例;内部使用来源键,显示文字仍保持简短,避免同名来源串联显隐。
|
const labels = Object.fromEntries(series.map(item=>[item.key,item.name]))
|
return {...option,tooltip:{trigger:'axis',renderMode:'richText',formatter:(entries:Array<{seriesName:string;value:[number,number|null]}>)=>entries.filter(entry=>entry.value[1]!=null).map(entry=>`${labels[entry.seriesName]??entry.seriesName}: ${entry.value[1]} ${metric.unit}`).join('\n')},legend:{...option.legend,formatter:(key:string)=>labels[key]??key,selected:Object.fromEntries(series.map(item=>[item.key,!item.hidden]))},series:option.series.map((line,index)=>({...line,name:series[index]!.key}))}
|
}
|