brickwise 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brickwise",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "App local para analizar la rentabilidad de alquiler de inmuebles de Property Finder UAE con el histórico real de transacciones DLD.",
5
5
  "license": "MIT",
6
6
  "author": "eljommys",
@@ -28,6 +28,7 @@ const IH = H - M.top - M.bottom;
28
28
  const ACCENT = "#2563eb"; // comparable (same bedrooms)
29
29
  const MUTED = "#9ca3af"; // other units
30
30
  const REF = "#059669"; // this listing
31
+ const TREND = "#7c3aed"; // tendency line
31
32
 
32
33
  function niceTicks(min: number, max: number, count: number): number[] {
33
34
  if (min === max) return [min];
@@ -77,18 +78,43 @@ export default function PsqftChart({ buy, rent, listingBedrooms, listingPsqft }:
77
78
  const refY = mode === "buy" ? listingPsqft ?? null : null;
78
79
  const tMin = Math.min(...ts);
79
80
  const tMax = Math.max(...ts);
80
- let yMin = Math.min(...ys, ...(refY ? [refY] : []));
81
- let yMax = Math.max(...ys, ...(refY ? [refY] : []));
81
+
82
+ // Least-squares trend over comparable units (fallback to all) the tendency.
83
+ const comp = pts.filter((p) => p.comparable);
84
+ const fitSet = comp.length >= 2 ? comp : pts;
85
+ let trend: { y0: number; y1: number; annualPct: number | null } | null = null;
86
+ if (fitSet.length >= 2 && tMax > tMin) {
87
+ const n = fitSet.length;
88
+ const mt = fitSet.reduce((s, p) => s + p.t, 0) / n;
89
+ const my = fitSet.reduce((s, p) => s + p.y, 0) / n;
90
+ let num = 0;
91
+ let den = 0;
92
+ for (const p of fitSet) {
93
+ num += (p.t - mt) * (p.y - my);
94
+ den += (p.t - mt) ** 2;
95
+ }
96
+ const slope = den ? num / den : 0; // AED/sqft per ms
97
+ const intercept = my - slope * mt;
98
+ const YEAR = 365.25 * 24 * 3600 * 1000;
99
+ trend = {
100
+ y0: intercept + slope * tMin,
101
+ y1: intercept + slope * tMax,
102
+ annualPct: my ? (slope * YEAR) / my : null,
103
+ };
104
+ }
105
+
106
+ const extra = [...(refY ? [refY] : []), ...(trend ? [trend.y0, trend.y1] : [])];
107
+ let yMin = Math.min(...ys, ...extra);
108
+ let yMax = Math.max(...ys, ...extra);
82
109
  const pad = (yMax - yMin) * 0.1 || yMax * 0.1 || 1;
83
110
  yMin = Math.max(0, yMin - pad);
84
111
  yMax = yMax + pad;
85
112
  const x = (t: number) => (tMax === tMin ? IW / 2 : ((t - tMin) / (tMax - tMin)) * IW);
86
113
  const y = (v: number) => IH - ((v - yMin) / (yMax - yMin)) * IH;
87
114
  // median of comparable set (fallback to all)
88
- const comp = pts.filter((p) => p.comparable);
89
115
  const base = (comp.length ? comp : pts).map((p) => p.y).sort((a, b) => a - b);
90
116
  const median = base.length ? base[Math.floor(base.length / 2)] : null;
91
- return { x, y, tMin, tMax, yMin, yMax, refY, median };
117
+ return { x, y, tMin, tMax, yMin, yMax, refY, median, trend };
92
118
  }, [pts, mode, listingPsqft]);
93
119
 
94
120
  const compCount = pts.filter((p) => p.comparable).length;
@@ -181,24 +207,44 @@ export default function PsqftChart({ buy, rent, listingBedrooms, listingPsqft }:
181
207
  </>
182
208
  )}
183
209
 
184
- {/* points: muted first, comparable on top */}
210
+ {/* trend (least-squares) line */}
211
+ {scales.trend && (
212
+ <line
213
+ x1={scales.x(scales.tMin)}
214
+ x2={scales.x(scales.tMax)}
215
+ y1={scales.y(scales.trend.y0)}
216
+ y2={scales.y(scales.trend.y1)}
217
+ stroke={TREND}
218
+ strokeWidth={2.5}
219
+ strokeLinecap="round"
220
+ />
221
+ )}
222
+
223
+ {/* points: muted first, comparable on top; small dot + invisible hit area */}
185
224
  {pts
186
225
  .slice()
187
226
  .sort((a, b) => Number(a.comparable) - Number(b.comparable))
188
227
  .map((p, i) => (
189
- <circle
190
- key={i}
191
- cx={scales.x(p.t)}
192
- cy={scales.y(p.y)}
193
- r={p.comparable ? 5 : 3.5}
194
- fill={p.comparable ? ACCENT : "none"}
195
- stroke={p.comparable ? "#fff" : MUTED}
196
- strokeWidth={p.comparable ? 1.5 : 1.5}
197
- opacity={p.comparable ? 1 : 0.7}
198
- onMouseEnter={() => setHover({ p, x: scales.x(p.t), y: scales.y(p.y) })}
199
- onMouseLeave={() => setHover(null)}
200
- style={{ cursor: "pointer" }}
201
- />
228
+ <g key={i}>
229
+ <circle
230
+ cx={scales.x(p.t)}
231
+ cy={scales.y(p.y)}
232
+ r={p.comparable ? 3 : 2}
233
+ fill={p.comparable ? ACCENT : MUTED}
234
+ stroke="#fff"
235
+ strokeWidth={p.comparable ? 1 : 0.5}
236
+ opacity={p.comparable ? 1 : 0.7}
237
+ />
238
+ <circle
239
+ cx={scales.x(p.t)}
240
+ cy={scales.y(p.y)}
241
+ r={8}
242
+ fill="transparent"
243
+ onMouseEnter={() => setHover({ p, x: scales.x(p.t), y: scales.y(p.y) })}
244
+ onMouseLeave={() => setHover(null)}
245
+ style={{ cursor: "pointer" }}
246
+ />
247
+ </g>
202
248
  ))}
203
249
  </g>
204
250
  </svg>
@@ -235,6 +281,16 @@ export default function PsqftChart({ buy, rent, listingBedrooms, listingPsqft }:
235
281
  <span className="inline-block h-0 w-4 border-t-[1.5px] border-dashed" style={{ borderColor: MUTED }} />
236
282
  Mediana
237
283
  </span>
284
+ {scales.trend && (
285
+ <span className="flex items-center gap-1.5">
286
+ <span className="inline-block h-0 w-4 border-t-[2.5px]" style={{ borderColor: TREND }} />
287
+ <span style={{ color: TREND }} className="font-semibold">
288
+ Tendencia
289
+ {scales.trend.annualPct != null &&
290
+ ` (${scales.trend.annualPct >= 0 ? "+" : ""}${(scales.trend.annualPct * 100).toFixed(1)} %/año)`}
291
+ </span>
292
+ </span>
293
+ )}
238
294
  {mode === "buy" && listingPsqft != null && (
239
295
  <span className="flex items-center gap-1.5">
240
296
  <span className="inline-block h-0 w-4 border-t-2" style={{ borderColor: REF }} />