מתי צריך ויזואליזציה מותאמת אישית עם Visx?
בקשה טיפוסית של לקוח: הצגת דינמיקת מחירי מניות בשנה האחרונה עם זום וטולטיפים. Recharts לא יכול להתמודד עם אנימציות לא שגרתיות, Chart.js מאט על 50,000 נקודות. צריך ויזואליזציה מותאמת אישית עם Visx — ספרייה מ-Airbnb המשלבת את הגמישות של D3 עם פרדיגמת React. Visx מאפשרת בניית כל תרשים SVG עם שליטה מלאה וביצועים גבוהים. אנחנו משתמשים בה בפרויקטים עם מערכי נתונים של עד 100,000 רשומות, ומבטיחים אנימציה חלקה ועדכונים מיידיים.
אילו בעיות Visx פותרת?
התאמה אישית מוגבלת של ספריות מוכנות
Recharts מציעה הרבה הגדרות, אבל כל סטייה מהתבנית הופכת לפריצה עם עקיפת פרופס. Visx נותנת שליטה מלאה על SVG: אתה מנהל כל אלמנט, סקאלה ומעבר. עם Visx, אפשר להשיג פי 5 יותר התאמה אישית מאשר Recharts לויזואליזציות מורכבות.
ביצועים על נפחי נתונים גדולים
רינדור D3 דרך מחזור החיים של React יכול לגרום ל-re-renders מיותרים. Visx מייעלת זאת עם רכיבים טהורים ו-memoization. על מערכי נתונים של 10,000 נקודות, Visx מספקת 60 FPS — עד פי 3 מהר יותר מ-Chart.js.
אינטגרציה עם אפליקציית React
רכיבי Visx הם רכיבי React רגילים שמשתלבים בקלות בכל ארכיטקטורה: Next.js, CRA, Vite. אין צורך ב-renderer נפרד או קונטקסט. לצוות שלנו יש ניסיון של 5+ שנים בבניית ויזואליזציות React, מה שמבטיח קוד באיכות גבוהה ומסירה בזמן.
איך Visx פותרת את משימת ההתאמה האישית?
Visx מספקת פרימיטיבים ברמה נמוכה: scaleTime, scaleLinear, LinePath, AreaClosed, AxisLeft, AxisBottom. אפשר להרכיב כל תרשים מאלה ללא תלות בתבניות. לאינטראקטיביות — הוק useTooltip ואירועי עכבר. כל הקוד הוא TypeScript עם נתונים מטיפוסים.
ניקח משימה טיפוסית: תרשים קו עם טולטיפ ואזור ממולא. דוגמת יישום ב-Visx (קטע קוד):
import { scaleTime, scaleLinear } from '@visx/scale';
import { LinePath, AreaClosed } from '@visx/shape';
import { AxisLeft, AxisBottom } from '@visx/axis';
import { GridRows, GridColumns } from '@visx/grid';
import { useTooltip, TooltipWithBounds, defaultStyles } from '@visx/tooltip';
import { localPoint } from '@visx/event';
import { bisector } from 'd3-array';
import { curveMonotoneX } from 'd3-shape';
interface DataPoint {
date: Date;
value: number;
}
const bisectDate = bisector<DataPoint, Date>(d => d.date).left;
function CustomLineChart({
data,
width,
height,
margin = { top: 20, right: 20, bottom: 40, left: 60 }
}: {
data: DataPoint[];
width: number;
height: number;
margin?: { top: number; right: number; bottom: number; left: number };
}) {
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = scaleTime({
range: [0, innerWidth],
domain: [
Math.min(...data.map(d => d.date.getTime())),
Math.max(...data.map(d => d.date.getTime()))
]
});
const yScale = scaleLinear({
range: [innerHeight, 0],
domain: [0, Math.max(...data.map(d => d.value)) * 1.1],
nice: true
});
const { tooltipData, tooltipLeft, tooltipTop, showTooltip, hideTooltip } = useTooltip<DataPoint>();
const handleTooltip = (event: React.MouseEvent<SVGRectElement>) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = xScale.invert(x - margin.left);
const index = bisectDate(data, x0, 1);
const d0 = data[index - 1];
const d1 = data[index];
const d = !d1 || Math.abs(x0.getTime() - d0.date.getTime()) < Math.abs(x0.getTime() - d1.date.getTime()) ? d0 : d1;
showTooltip({
tooltipData: d,
tooltipLeft: xScale(d.date) + margin.left,
tooltipTop: yScale(d.value) + margin.top
});
};
return (
<div style={{ position: 'relative' }}>
<svg width={width} height={height}>
<g transform={`translate(${margin.left}, ${margin.top})`}>
<GridRows scale={yScale} width={innerWidth} stroke="#f0f0f0" />
<GridColumns scale={xScale} height={innerHeight} stroke="#f0f0f0" />
<defs>
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<AreaClosed
data={data}
x={d => xScale(d.date)}
y={d => yScale(d.value)}
yScale={yScale}
fill="url(#areaGradient)"
curve={curveMonotoneX}
/>
<LinePath
data={data}
x={d => xScale(d.date)}
y={d => yScale(d.value)}
stroke="#3b82f6"
strokeWidth={2}
curve={curveMonotoneX}
/>
<AxisLeft scale={yScale} tickFormat={v => `${(v as number / 1000).toFixed(0)}k`} />
<AxisBottom top={innerHeight} scale={xScale} tickFormat={d => format(d as Date, 'dd MMM')} />
<rect
width={innerWidth}
height={innerHeight}
fill="transparent"
onMouseMove={handleTooltip}
onMouseLeave={hideTooltip}
/>
{tooltipData && (
<g>
<line
x1={tooltipLeft! - margin.left}
x2={tooltipLeft! - margin.left}
y1={0}
y2={innerHeight}
stroke="#3b82f6"
strokeDasharray="4,4"
strokeWidth={1}
/>
<circle
cx={tooltipLeft! - margin.left}
cy={tooltipTop! - margin.top}
r={5}
fill="#3b82f6"
stroke="white"
strokeWidth={2}
/>
</g>
)}
</g>
</svg>
{tooltipData && (
<TooltipWithBounds
top={tooltipTop}
left={tooltipLeft}
style={{ ...defaultStyles, background: '#1e293b', color: 'white' }}
>
<div>
<strong>{format(tooltipData.date, 'dd.MM.yyyy')}</strong>
<br />
{tooltipData.value.toLocaleString('ru')} $
</div>
</TooltipWithBounds>
)}
</div>
);
} ParentSize לרספונסיביות
import { ParentSize } from '@visx/responsive';
function ResponsiveChart({ data }) {
return (
<ParentSize>
{({ width, height }) => (
<CustomLineChart data={data} width={width} height={height || 300} />
)}
</ParentSize>
);
} למה Visx ולא Chart.js?
Visx עולה על Chart.js במצבים הדורשים צורות תרשים לא סטנדרטיות או תרחישים אינטראקטיביים מורכבים. Chart.js קל יותר להגדרה ומהיר יותר לתרשימים פשוטים, אבל הארכיטקטורה שלו מתקשה להתרחב למשימות מותאמות אישית. Visx מספקת גישה מלאה ל-SVG, ומאפשרת כל ויזואליזציה. הלקוחות שלנו חוסכים עד 40% מזמן הפיתוח לדשבורדים מורכבים באמצעות Visx. פיתוח ויזואליזציה מותאמת אישית טיפוסי מתחיל ב-$2,500 לסוג תרשים.
השוואת Visx מול Recharts
| קריטריון | Visx | Recharts |
|---|---|---|
| רמת שליטה | מלאה (פרימיטיבי SVG) | בינונית (הגדרת פרופס) |
| התאמה אישית | בלתי מוגבלת | מוגבלת על ידי תבניות |
| ביצועים | גבוהים (שליטה ידנית) | בינוניים (re-renders אוטומטיים) |
| זמן פיתוח | 3-5 ימים לסוג | 1-2 ימים לסוג |
| עקומת למידה | בינונית (דורש ידע ב-D3) | נמוכה (React בלבד) |
טעויות נפוצות בעבודה עם Visx
-
חוסר memoization של סקאלות. סקאלות צריכות להיווצר בתוך
import { scaleTime, scaleLinear } from '@visx/scale'; import { LinePath, AreaClosed } from '@visx/shape'; import { AxisLeft, AxisBottom } from '@visx/axis'; import { GridRows, GridColumns } from '@visx/grid'; import { useTooltip, TooltipWithBounds, defaultStyles } from '@visx/tooltip'; import { localPoint } from '@visx/event'; import { bisector } from 'd3-array'; import { curveMonotoneX } from 'd3-shape'; interface DataPoint { date: Date; value: number; } const bisectDate = bisector<DataPoint, Date>(d => d.date).left; function CustomLineChart({ data, width, height, margin = { top: 20, right: 20, bottom: 40, left: 60 } }: { data: DataPoint[]; width: number; height: number; margin?: { top: number; right: number; bottom: number; left: number }; }) { const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; const xScale = scaleTime({ range: [0, innerWidth], domain: [ Math.min(...data.map(d => d.date.getTime())), Math.max(...data.map(d => d.date.getTime())) ] }); const yScale = scaleLinear({ range: [innerHeight, 0], domain: [0, Math.max(...data.map(d => d.value)) * 1.1], nice: true }); const { tooltipData, tooltipLeft, tooltipTop, showTooltip, hideTooltip } = useTooltip<DataPoint>(); const handleTooltip = (event: React.MouseEvent<SVGRectElement>) => { const { x } = localPoint(event) || { x: 0 }; const x0 = xScale.invert(x - margin.left); const index = bisectDate(data, x0, 1); const d0 = data[index - 1]; const d1 = data[index]; const d = !d1 || Math.abs(x0.getTime() - d0.date.getTime()) < Math.abs(x0.getTime() - d1.date.getTime()) ? d0 : d1; showTooltip({ tooltipData: d, tooltipLeft: xScale(d.date) + margin.left, tooltipTop: yScale(d.value) + margin.top }); }; return ( <div style={{ position: 'relative' }}> <svg width={width} height={height}> <g transform={`translate(${margin.left}, ${margin.top})`}> <GridRows scale={yScale} width={innerWidth} stroke="#f0f0f0" /> <GridColumns scale={xScale} height={innerHeight} stroke="#f0f0f0" /> <defs> <linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stopColor="#3b82f6" stopOpacity={0.3} /> <stop offset="100%" stopColor="#3b82f6" stopOpacity={0} /> </linearGradient> </defs> <AreaClosed data={data} x={d => xScale(d.date)} y={d => yScale(d.value)} yScale={yScale} fill="url(#areaGradient)" curve={curveMonotoneX} /> <LinePath data={data} x={d => xScale(d.date)} y={d => yScale(d.value)} stroke="#3b82f6" strokeWidth={2} curve={curveMonotoneX} /> <AxisLeft scale={yScale} tickFormat={v => `${(v as number / 1000).toFixed(0)}k`} /> <AxisBottom top={innerHeight} scale={xScale} tickFormat={d => format(d as Date, 'dd MMM')} /> <rect width={innerWidth} height={innerHeight} fill="transparent" onMouseMove={handleTooltip} onMouseLeave={hideTooltip} /> {tooltipData && ( <g> <line x1={tooltipLeft! - margin.left} x2={tooltipLeft! - margin.left} y1={0} y2={innerHeight} stroke="#3b82f6" strokeDasharray="4,4" strokeWidth={1} /> <circle cx={tooltipLeft! - margin.left} cy={tooltipTop! - margin.top} r={5} fill="#3b82f6" stroke="white" strokeWidth={2} /> </g> )} </g> </svg> {tooltipData && ( <TooltipWithBounds top={tooltipTop} left={tooltipLeft} style={{ ...defaultStyles, background: '#1e293b', color: 'white' }} > <div> <strong>{format(tooltipData.date, 'dd.MM.yyyy')}</strong> <br /> {tooltipData.value.toLocaleString('ru')} $ </div> </TooltipWithBounds> )} </div> ); }או מחוץ לרכיב; אחרת, כל re-render יוצר אותן מחדש, מה שמוביל לאובדן ביצועים. - התעלמות מ-ParentSize. ללא קונטיינר אדפטיבי, התרשים לא יגיב לשינוי גודל החלון, מה שפוגע בחוויית המשתמש במכשירים ניידים.
- טיפול לא נכון בקצוות נתונים. אם הנתונים מכילים NaN או null, Visx עלולה לזרוק שגיאה. תמיד סנן נתונים לפני העברתם לרכיבים.
- שכחת נגישות. גרפיקת SVG צריכה להיות נגישה לקוראי מסך: הוסף
import { ParentSize } from '@visx/responsive'; function ResponsiveChart({ data }) { return ( <ParentSize> {({ width, height }) => ( <CustomLineChart data={data} width={width} height={height || 300} /> )} </ParentSize> ); }ו-scaleTime.
איך לבנות תרשים מותאם אישית מאפס?
- הגדר סקאלות —
scaleLinearלזמן,LinePathלערכים. - תאר גאומטריה —
AreaClosedלקו,AxisLeftלמילוי. - הוסף צירים —
AxisBottomו-useTooltip. - יישם טולטיפ — השתמש בהוק
ParentSizeובאירועי עכבר. - עטוף בקונטיינר אדפטיבי —
ParentSizeלשינוי גודל אוטומטי.
תהליך ולוח זמנים
| שלב | משך |
|---|---|
| ניתוח ואיסוף דרישות | 0.5–1 יום |
| אב-טיפוס MVP | 1–2 ימים |
| פיתוח גרסה סופית | 3–5 ימים לסוג |
| בקרת איכות ואופטימיזציית ביצועים | 1–2 ימים |
| פריסה ואינטגרציה | 0.5–1 יום |
המחזור המלא לסוג ויזואליזציה אחד אורך 3 עד 5 ימים. סט של 3-4 סוגים שונים אורך 1.5 עד 2 שבועות. העלות מחושבת באופן אישי בהתאם למורכבות האינטראקציה ונפח הנתונים.
מה כלול בעבודה?
אנחנו מספקים פיתוח ויזואליזציית נתונים סוהר על Visx:
- ניתוח דרישות ועיצוב פריסה;
- קידוד מאפס באמצעות React + TypeScript;
- בדיקות על נתונים אמיתיים (עד 100,000 רשומות);
- התאמה למסכים ניידים ולתצוגות רטינה;
- תיעוד ב-README והערות בקוד;
- העברת מאגר הקוד וגישת פריסה;
- הכשרת הצוות שלך על Visx (סמינר מקוון של שעה).
עם ניסיון של למעלה מ-5 שנים ו-50+ פרויקטים מוצלחים, אנחנו מבטיחים גרפיקה עסקית מותאמת אישית באיכות גבוהה. צור קשר כדי לדון במשימה שלך. קבל ייעוץ על ויזואליזציה מותאמת אישית.







