Skip to content

Commit

Permalink
feat: 支持自定义缺失值
Browse files Browse the repository at this point in the history
  • Loading branch information
LeafYeeXYZ committed Oct 9, 2024
1 parent bf65269 commit 9ffbdfa
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 12 deletions.
5 changes: 3 additions & 2 deletions src/components/DataView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { flushSync } from 'react-dom'

export function DataView() {

const { data, setData, dataCols, dataRows, ACCEPT_FILE_TYPES, messageApi } = useZustand()
const { data, setData, dataCols, dataRows, ACCEPT_FILE_TYPES, messageApi, setIsLargeData, LARGE_DATA_SIZE } = useZustand()
// 上传状态
const [uploading, setUploading] = useState<boolean>(false)

Expand Down Expand Up @@ -84,8 +84,9 @@ export function DataView() {
})
flushSync(() => setUploading(true))
// 如果文件比较大, 延迟等待通知加载
if (file.size > 3 * 1024 * 1024) {
if (file.size > LARGE_DATA_SIZE) {
await new Promise((resolve) => setTimeout(resolve, 500))
setIsLargeData(true)
}
const reader = new FileReader()
const ext = file.name.split('.').pop()?.toLowerCase()
Expand Down
95 changes: 86 additions & 9 deletions src/components/VariableView.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useZustand } from '../lib/useZustand'
import { Button, Table } from 'antd'
import { Button, Table, Modal, Select } from 'antd'
import { CalculatorOutlined, ZoomOutOutlined } from '@ant-design/icons'
import { useState } from 'react'
import { useState, useRef } from 'react'
import { flushSync } from 'react-dom'
import { utils } from 'xlsx'

export function VariableView() {

const { dataCols, setDataCols, dataRows, messageApi, CALCULATE_VARIABLES } = useZustand()
const { data, dataCols, setDataCols, dataRows, setDataRows, messageApi, CALCULATE_VARIABLES, isLargeData } = useZustand()
const [calculating, setCalculating] = useState<boolean>(false)
const handleCalculate = () => {
const handleCalculate = async () => {
try {
messageApi?.loading('正在处理数据...')
isLargeData && await new Promise((resolve) => setTimeout(resolve, 500))
const cols = CALCULATE_VARIABLES(dataCols, dataRows)
setDataCols(cols)
messageApi?.destroy()
Expand All @@ -24,6 +26,42 @@ export function VariableView() {
messageApi?.error(`数据处理失败: ${error instanceof Error ? error.message : JSON.stringify(error)}`)
}
}
// 处理缺失值
const handleMissingParams = useRef<{ variable?: string; missing?: unknown[] }>({})
const handleMissing = async (variable: string, missing?: unknown[]) => {
try {
messageApi?.loading('正在处理数据...')
isLargeData && await new Promise((resolve) => setTimeout(resolve, 500))
const cols = dataCols.map((col) => {
if (col.name === variable) {
return { ...col, missingValues: missing }
} else {
return col
}
})
const sheet = data!.Sheets[data!.SheetNames[0]]
const rows = (utils.sheet_to_json(sheet) as { [key: string]: unknown }[]).map((row) => {
const value = row[variable]
if (missing && missing.some((m) => value == m)) {
return { ...row, [variable]: undefined }
} else {
return row
}
})
setDataCols(CALCULATE_VARIABLES(cols, rows))
setDataRows(rows)
messageApi?.destroy()
messageApi?.open({
type: 'success',
content: '数据处理完成',
duration: 0.5,
})
} catch (error) {
messageApi?.destroy()
messageApi?.error(`数据处理失败: ${error instanceof Error ? error.message : JSON.stringify(error)}`)
}
}
const [modalApi, contextHolder] = Modal.useModal()

return (
<div className='w-full h-full overflow-hidden'>
Expand All @@ -32,19 +70,51 @@ export function VariableView() {
<div className='w-full flex justify-start items-center gap-3 mb-4'>
<Button
icon={<CalculatorOutlined />}
loading={calculating}
disabled={calculating}
onClick={() => {
onClick={async () => {
flushSync(() => setCalculating(true))
handleCalculate()
await handleCalculate()
flushSync(() => setCalculating(false))
}}
>
重新计算描述统计量
</Button>
<Button
icon={<ZoomOutOutlined />}
disabled={calculating || true}
disabled={calculating}
onClick={async () => {
flushSync(() => setCalculating(true))
await modalApi.confirm({
title: '手动定义变量缺失值',
content: (
<div className='flex flex-col gap-4 my-4'>
<Select
placeholder='请选择变量'
defaultValue={handleMissingParams.current.variable}
onChange={(value) => handleMissingParams.current.variable = value as string}
>
{dataCols.map((col) => (
<Select.Option key={col.name} value={col.name}>{col.name}</Select.Option>
))}
</Select>
<Select
mode='tags'
placeholder='请输入缺失值 (可为多个值/为空)'
defaultValue={handleMissingParams.current.missing}
onChange={(value) => handleMissingParams.current.missing = value?.length > 0 ? value : undefined}
/>
</div>
),
onOk: async () => {
if (handleMissingParams.current.variable) {
await handleMissing(handleMissingParams.current.variable, handleMissingParams.current.missing)
}
},
okText: '确定',
cancelText: '取消',
})
flushSync(() => setCalculating(false))
}}
>
手动定义变量缺失值
</Button>
Expand All @@ -53,11 +123,17 @@ export function VariableView() {
<Table
className='w-full overflow-auto text-nowrap'
bordered
dataSource={dataCols}
dataSource={dataCols.map((col) => {
return {
...col,
missingValues: col.missingValues?.join(', '),
}
})}
columns={[
{ title: '变量名', dataIndex: 'name', key: 'name', width: '6rem' },
{ title: '数据类型', dataIndex: 'type', key: 'type', width: '6rem' },
{ title: '样本量', dataIndex: 'count', key: 'count', width: '6rem' },
{ title: '缺失值定义', dataIndex: 'missingValues', key: 'missingValues', width: '7rem' },
{ title: '缺失值数量', dataIndex: 'missing', key: 'missing', width: '7rem' },
{ title: '有效值数量', dataIndex: 'valid', key: 'valid', width: '7rem' },
{ title: '唯一值数量', dataIndex: 'unique', key: 'unique', width: '7rem' },
Expand All @@ -77,6 +153,7 @@ export function VariableView() {
}}
/>
</div>
{contextHolder}
</div>
)
}
16 changes: 15 additions & 1 deletion src/lib/useZustand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const CALCULATE_VARIABLES = (dataCols: Variable[], dataRows: { [key: string]: un
})
return cols
}
const LARGE_DATA_SIZE = 3 * 1024 * 1024

type Variable = {
/** 变量名 */
Expand Down Expand Up @@ -68,7 +69,13 @@ type Variable = {
q3?: number
/** 标准差 */
std?: number
/** 自定义的缺失值 */
/**
* 自定义的缺失值
* 默认为空, 即只把 undefined 作为缺失值
* 在 VariableView.tsx 中改变后, 将把缺失值为 missingValues 的数据项置为 undefined
* 同时, 在比较时故意使用 == 而不是 ===, 以规避数字和字符串的比较问题
* 缺失值设置只改变 dataRows 和 dataCols 的值, 不改变 data 的值
*/
missingValues?: unknown[]
}

Expand All @@ -83,6 +90,10 @@ type State = {
dataCols: Variable[]
setDataCols: (cols: Variable[]) => void
setDataRows: (rows: { [key: string]: unknown }[]) => void
// 是否数据量过大
LARGE_DATA_SIZE: number
isLargeData: boolean
setIsLargeData: (isLarge: boolean) => void
// 可打开的文件类型
ACCEPT_FILE_TYPES: string[]
// 计算变量描述统计量的函数
Expand All @@ -98,6 +109,8 @@ export const useZustand = create<State>()((set) => ({
data: null,
dataRows: [],
dataCols: [],
isLargeData: false,
setIsLargeData: (isLarge) => set({ isLargeData: isLarge }),
setData: (data) => {
if (data) {
const sheet = data.Sheets[data.SheetNames[0]]
Expand All @@ -116,6 +129,7 @@ export const useZustand = create<State>()((set) => ({
setDataRows: (rows) => set({ dataRows: rows }),
ACCEPT_FILE_TYPES,
CALCULATE_VARIABLES,
LARGE_DATA_SIZE,
messageApi: null,
setMessageApi: (api) => set({ messageApi: api }),
}))

0 comments on commit 9ffbdfa

Please sign in to comment.