Skip to content

Commit

Permalink
add shadow and TextIfFits in charting
Browse files Browse the repository at this point in the history
  • Loading branch information
JafarMirzaie committed Mar 18, 2022
1 parent 48615ea commit 740b67b
Show file tree
Hide file tree
Showing 19 changed files with 536 additions and 530 deletions.
6 changes: 5 additions & 1 deletion Signum.React.Extensions/Chart/Chart.css
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ body.rtl .sf-chart-token .sf-query-token {
}

.sf-chart-animable .sf-transition {
transition: all 0.3s cubic-bezier(0.92, 0.02, 0.21, 1.01)
transition: all 0.3s cubic-bezier(0.92, 0.02, 0.21, 1.01), filter 0.1s linear
}

.shadow-group:hover .shadow {
filter: drop-shadow( 1px 1px 3px rgba(0, 0, 0, .4));
}

text.sf-initial-message {
Expand Down
160 changes: 75 additions & 85 deletions Signum.React.Extensions/Chart/D3Scripts/Bars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import * as ChartClient from '../ChartClient';
import * as ChartUtils from './Components/ChartUtils';
import { translate, scale, rotate, skewX, skewY, matrix, scaleFor } from './Components/ChartUtils';
import { ChartRow, ChartScriptProps } from '../ChartClient';
import TextEllipsis from './Components/TextEllipsis';
import { XKeyTicks, YScaleTicks, YKeyTicks, XScaleTicks } from './Components/Ticks';
import { XAxis, YAxis } from './Components/Axis';
import { Rule } from './Components/Rule';
import InitialMessage from './Components/InitialMessage';
import TextIfFits from './Components/TextIfFits';
import TextEllipsis from './Components/TextEllipsis';


export default function renderBars({ data, width, height, parameters, loading, onDrillDown, initialLoad, chartRequest, memo, dashboardFilter }: ChartScriptProps): React.ReactElement<any> {
Expand Down Expand Up @@ -70,6 +71,8 @@ export default function renderBars({ data, width, height, parameters, loading, o

var detector = dashboardFilter?.getActiveDetector(chartRequest);

const bandMargin = y.bandwidth() > 20 ? 2 : 0;

return (
<svg direction="ltr" width={width} height={height}>
<g opacity={dashboardFilter ? .5 : undefined}>
Expand All @@ -79,98 +82,85 @@ export default function renderBars({ data, width, height, parameters, loading, o

{/*PAINT GRAPH*/}
<g className="shape" transform={translate(xRule.start('content'), yRule.start('content'))}>
{orderedRows.map(r => {
{keyValues.map(k => {

var active = detector?.(r);
var key = keyColumn.getKey(k);

var key = keyColumn.getValueKey(r);
var row: ChartRow | undefined = rowsByKey[key];
var active = detector?.(row);

return (
<rect key={key} className="shape sf-transition"
opacity={active == false ? .5 : undefined}
stroke={active == true ? "black" : y.bandwidth() > 4 ? '#fff' : undefined}
strokeWidth={active == true ? 3 : undefined}
transform={translate(0, y(key)!) + (initialLoad ? scale(0, 1) : scale(1, 1))}
width={x(valueColumn.getValue(r))}
height={y.bandwidth()}
fill={keyColumn.getValueColor(r) ?? color(key)}
onClick={e => onDrillDown(r, e)}
cursor="pointer">
<title>
{keyColumn.getValueNiceName(r) + ': ' + valueColumn.getValueNiceName(r)}
</title>
</rect>
);
})}
</g>
var posx = x(row ? valueColumn.getValue(row) : 0)!;

{y.bandwidth() > 15 &&
(isMargin ?
<g className="y-label" transform={translate(xRule.end('labels'), yRule.start('content') + y.bandwidth() / 2)}>
{(isAll ? keyValues : orderedRows.map(r => keyColumn.getValue(r))).map(k => <TextEllipsis key={keyColumn.getKey(k)}
transform={translate(0, y(keyColumn.getKey(k))!)}
maxWidth={xRule.size('labels')}
padding={labelMargin}
className="y-label sf-transition"
fill={(keyColumn.getColor(k) ?? color(keyColumn.getKey(k)))}
dominantBaseline="middle"
textAnchor="end"
fontWeight="bold"
onClick={e => onDrillDown({ c0: k }, e)}
cursor="pointer">
{keyColumn.getNiceName(k)}
</TextEllipsis>)}
</g> :
isInside ?
<g className="y-label" transform={translate(xRule.start('content') + labelMargin, yRule.start('content') + y.bandwidth() / 2)}>
{(isAll ? keyValues : orderedRows.map(r => keyColumn.getValue(r))).map(k => {

var row = rowsByKey[keyColumn.getKey(k)];

var posx = x(row ? valueColumn.getValue(row) : 0)!;
return (
<TextEllipsis key={keyColumn.getKey(k)}
transform={translate(posx >= size / 2 ? 0 : posx, y(keyColumn.getKey(k))!)}
maxWidth={posx >= size / 2 ? posx : size - posx}
return (
<g className="shadow-group" key={key}>
{row && <rect className="shape sf-transition shadow"
opacity={active == false ? .5 : undefined}
transform={translate(0, y(key)! + bandMargin) + (initialLoad ? scale(0, 1) : scale(1, 1))}
width={x(valueColumn.getValue(row))}
height={y.bandwidth() - bandMargin * 2}
fill={keyColumn.getValueColor(row) ?? color(key)}
onClick={e => onDrillDown(row!, e)}
cursor="pointer">
<title>
{keyColumn.getValueNiceName(row) + ': ' + valueColumn.getValueNiceName(row)}
</title>
</rect>
}
{y.bandwidth() > 15 && (isAll || row != null) &&
(isMargin ?
<g className="y-label" transform={translate(-labelMargin, y.bandwidth() / 2)}>
<TextEllipsis
transform={translate(0, y(keyColumn.getKey(key))!)}
maxWidth={xRule.size('labels')}
padding={labelMargin}
className="y-label sf-transition"
fill={(keyColumn.getColor(key) ?? color(keyColumn.getKey(key)))}
dominantBaseline="middle"
textAnchor="end"
fontWeight="bold"
onClick={e => onDrillDown({ c0: key }, e)}
cursor="pointer">
{keyColumn.getNiceName(key)}
</TextEllipsis>)
</g> :
isInside ?
<g className="y-label" transform={translate(labelMargin, y.bandwidth() / 2)}>
<TextEllipsis
transform={translate(posx >= size / 2 ? 0 : posx, y(keyColumn.getKey(key))!)}
maxWidth={posx >= size / 2 ? posx : size - posx}
padding={labelMargin}
className="y-label sf-transition"
fill={posx >= size / 2 ? '#fff' : (keyColumn.getColor(key) ?? color(keyColumn.getKey(key)))}
dominantBaseline="middle"
fontWeight="bold"
onClick={e => onDrillDown({ c0: key }, e)}
cursor="pointer">
{keyColumn.getNiceName(key)}
</TextEllipsis>
</g> : null
)}
{y.bandwidth() > 15 && parseFloat(parameters["NumberOpacity"]) > 0 && row &&
<g className="numbers-label">
<TextIfFits
transform={translate(x(valueColumn.getValue(row))! / 2, y(keyColumn.getValueKey(row))! + y.bandwidth() / 2)}
maxWidth={x(valueColumn.getValue(row))!}
padding={labelMargin}
className="y-label sf-transition"
fill={posx >= size / 2 ? '#fff' : (keyColumn.getColor(k) ?? color(keyColumn.getKey(k)))}
className="number-label sf-transition"
fill={parameters["NumberColor"] ?? "#000"}
dominantBaseline="middle"
opacity={parameters["NumberOpacity"]}
textAnchor="middle"
fontWeight="bold"
onClick={e => onDrillDown({ c0: k }, e)}
onClick={e => onDrillDown(row!, e)}
cursor="pointer">
{keyColumn.getNiceName(k)}
</TextEllipsis>
);
})}
</g> : null
)}

{y.bandwidth() > 15 && parseFloat(parameters["NumberOpacity"]) > 0 &&
<g className="numbers-label" transform={translate(xRule.start('content'), yRule.start('content'))}>
{orderedRows
.filter(r => x(valueColumn.getValue(r))! > 20)
.map(r => {
var posx = x(valueColumn.getValue(r))!;

return (<TextEllipsis key={keyColumn.getValueKey(r)}
transform={translate(x(valueColumn.getValue(r))! / 2, y(keyColumn.getValueKey(r))! + y.bandwidth() / 2)}
maxWidth={posx >= size / 2 ? posx : size - posx}
padding={labelMargin}
className="number-label sf-transition"
fill={parameters["NumberColor"] ?? "#000"}
dominantBaseline="middle"
opacity={parameters["NumberOpacity"]}
textAnchor="middle"
fontWeight="bold"
onClick={e => onDrillDown(r, e)}
cursor="pointer">
{valueColumn.getValueNiceName(r)}
</TextEllipsis>);
})}
</g>
}

{valueColumn.getValueNiceName(row)}
</TextIfFits>
</g>
}
</g>
);
})}
</g>
<InitialMessage data={data} x={xRule.middle("content")} y={yRule.middle("content")} loading={loading} />
<g opacity={dashboardFilter ? .5 : undefined}>
<XAxis xRule={xRule} yRule={yRule} />
Expand Down
68 changes: 34 additions & 34 deletions Signum.React.Extensions/Chart/D3Scripts/BubblePack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,40 +83,40 @@ export default function renderBubblePack({ data, width, height, parameters, load
nodes.orderByDescending(a => a.r).map(d => {
const active = activeDetector?.(isFolder(d.data) ? ({ c2: d.data.folder }) : d.data);
return (
<g key={getNodeKey(d)} className="node sf-transition" transform={translate(d.x, d.y) + (initialLoad ? scale(0, 0) : scale(1, 1))} cursor="pointer"
onClick={e => isFolder(d.data) ? onDrillDown({ c2: d.data.folder }, e) : onDrillDown(d.data, e)}>
<circle className="sf-transition" shapeRendering="initial" r={d.r}
opacity={active == false ? .5 : undefined}
fill={isFolder(d.data) ? folderColor!(d.data.folder) : color(d.data)!}
fillOpacity={parameters["FillOpacity"] ?? undefined}
stroke={active == true ? "black" : parameters["StrokeColor"] ?? (isFolder(d.data) ? folderColor!(d.data.folder) : (color(d.data) ?? undefined))}
strokeWidth={parameters["StrokeWidth"]} strokeOpacity={1} />
{!isFolder(d.data) &&
<TextEllipsis maxWidth={d.r * 2} padding={1} etcText=""
dominantBaseline="middle" textAnchor="middle" dy={showNumber && d.r > numberSizeLimit ? "-0.5em" : undefined}>
{keyColumn.getValueNiceName(d.data as ChartRow)}
</TextEllipsis>
}
{showNumber && d.r > numberSizeLimit && !isFolder(d.data) &&
<text fill={parameters["NumberColor"] ?? "#000"}
dominantBaseline="middle"
textAnchor="middle"
fontWeight="bold"
opacity={parseFloat(parameters["NumberOpacity"]) * d.r / 30}
dy=".5em">
{valueColumn.getValueNiceName(d.data as ChartRow)}
</text>
}
<title>
{isFolder(d.data) ? parentColumn!.getNiceName(d.data.folder) :
(keyColumn.getValueNiceName(d.data as ChartRow) + (parentColumn == null ? '' : (' (' + parentColumn.getValueNiceName(d.data as ChartRow) + ')')))}:
{isFolder(d.data) ? format(size.invert(d.value!)) :
(valueColumn.getValueNiceName(d.data)
+ (colorScaleColumn == null ? '' : (' (' + colorScaleColumn.getValueNiceName(d.data) + ')'))
+ (colorSchemeColumn == null ? '' : (' (' + colorSchemeColumn.getValueNiceName(d.data) + ')'))
)}
</title>
</g>);
<g key={getNodeKey(d)} className="node sf-transition shadow-group" transform={translate(d.x, d.y) + (initialLoad ? scale(0, 0) : scale(1, 1))} cursor="pointer"
onClick={e => isFolder(d.data) ? onDrillDown({ c2: d.data.folder }, e) : onDrillDown(d.data, e)}>
<circle className="sf-transition shadow" shapeRendering="initial" r={d.r}
opacity={active == false ? .5 : undefined}
fill={isFolder(d.data) ? folderColor!(d.data.folder) : color(d.data)!}
fillOpacity={parameters["FillOpacity"] ?? undefined}
stroke={active == true ? "black" : parameters["StrokeColor"] ?? (isFolder(d.data) ? folderColor!(d.data.folder) : (color(d.data) ?? undefined))}
strokeWidth={parameters["StrokeWidth"]} strokeOpacity={1} />
{!isFolder(d.data) &&
<TextEllipsis maxWidth={d.r * 2} padding={1} etcText=""
dominantBaseline="middle" textAnchor="middle" dy={showNumber && d.r > numberSizeLimit ? "-0.5em" : undefined}>
{keyColumn.getValueNiceName(d.data as ChartRow)}
</TextEllipsis>
}
{showNumber && d.r > numberSizeLimit && !isFolder(d.data) &&
<text fill={parameters["NumberColor"] ?? "#000"}
dominantBaseline="middle"
textAnchor="middle"
fontWeight="bold"
opacity={parseFloat(parameters["NumberOpacity"]) * d.r / 30}
dy=".5em">
{valueColumn.getValueNiceName(d.data as ChartRow)}
</text>
}
<title>
{isFolder(d.data) ? parentColumn!.getNiceName(d.data.folder) :
(keyColumn.getValueNiceName(d.data as ChartRow) + (parentColumn == null ? '' : (' (' + parentColumn.getValueNiceName(d.data as ChartRow) + ')')))}:
{isFolder(d.data) ? format(size.invert(d.value!)) :
(valueColumn.getValueNiceName(d.data)
+ (colorScaleColumn == null ? '' : (' (' + colorScaleColumn.getValueNiceName(d.data) + ')'))
+ (colorSchemeColumn == null ? '' : (' (' + colorSchemeColumn.getValueNiceName(d.data) + ')'))
)}
</title>
</g>);
})
}
<InitialMessage data={data} x={width / 2} y={height / 2} loading={loading} />
Expand Down
4 changes: 2 additions & 2 deletions Signum.React.Extensions/Chart/D3Scripts/Bubbleplot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ export default function renderBubbleplot({ data, width, height, parameters, load

return (
<g key={keyColumns.map(c => c.getValueKey(r)).join("/")}
className="shape-serie sf-transition"
className="shape-serie sf-transition shadow-group"
opacity={active == false ? .5 : undefined}
transform={translate(x(horizontalColumn.getValue(r))!, -y(verticalColumn.getValue(r))!) + (initialLoad ? scale(0, 0) : scale(1, 1))}
cursor="pointer"
onClick={e => onDrillDown(r, e)}
>
<circle className="shape sf-transition"
<circle className="shape sf-transition shadow"
stroke={active == true ? "black" : colorKeyColumn.getValueColor(r) ?? color(r)}
strokeWidth={3} fill={colorKeyColumn.getValueColor(r) ?? color(r)}
fillOpacity={parseFloat(parameters["FillOpacity"])}
Expand Down
44 changes: 24 additions & 20 deletions Signum.React.Extensions/Chart/D3Scripts/CalendarStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,26 +231,30 @@ export function CalendarYear({ year, rules, rowByDate, width, height, onDrillDow
</g>

<g transform={translate(rules.daysRule.start("daysContent"), rules.weeksRule.start("weeksContent"))}>
{d3.utcDays(new Date(Date.UTC(year, 0, 1)), new Date(Date.UTC(year + 1, 0, 1))).map(d => {
const r: ChartRow | undefined = rowByDate[cleanDate(d)];
const active = r && detector?.(r);
return <rect key={d.toISOString()}
className="sf-transition"
opacity={active == false ? .5 : undefined}
stroke={active == true ? "black" : "#ccc"}
strokeWidth={active == true ? 2 : undefined}
fill={r == undefined || initialLoad ? "#fff" : color(r)}
width={cellSize}
height={cellSize}
x={day(d) * cellSize}
y={week(d) * cellSize}
cursor="pointer"
onClick={e => r == undefined ? null : onDrillDown(r, e)}>
<title>
{dateFormat(d) + (r == undefined ? "" : ("(" + valueColumn.getValueNiceName(r) + ")"))}
</title>
</rect>
})}
{d3.utcDays(new Date(Date.UTC(year, 0, 1)), new Date(Date.UTC(year + 1, 0, 1))).map(d => {
const r: ChartRow | undefined = rowByDate[cleanDate(d)];
const active = r && detector?.(r);
return (
<g className="shadow-group" key={d.toISOString()}>
<rect
className="sf-transition shadow"
opacity={active == false ? .5 : undefined}
stroke={active == true ? "black" : "#ccc"}
strokeWidth={active == true ? 2 : undefined}
fill={r == undefined || initialLoad ? "#fff" : color(r)}
width={cellSize}
height={cellSize}
x={day(d) * cellSize}
y={week(d) * cellSize}
cursor="pointer"
onClick={e => r == undefined ? null : onDrillDown(r, e)}>
<title>
{dateFormat(d) + (r == undefined ? "" : ("(" + valueColumn.getValueNiceName(r) + ")"))}
</title>
</rect>
</g>
)
})}
</g>

<g transform={translate(rules.daysRule.start("daysContent"), rules.weeksRule.start("weeksContent"))} opacity={detector ? .5 : undefined} >
Expand Down
Loading

1 comment on commit 740b67b

@olmobrutall
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improvements in Charting

Thanks to @JafarMirzaie chartings have been improved in three areas:

  1. CSS Hover effect on each clickable element:
    By using CSS on SVG we are limited to the possible effects, but we don't need to make a React re-render to it's very fast.
    Chart
    All the SVG charts have been updated!.

  2. TextIfFits allows to prevent text labels that could not fit in the availabled space and could not be make smaller using ellipsis (like numbers).
    image

  3. Default format for Sum is now "0.#K" that get's translated to Javascript to Intl.NumberFormat({ notation: "compact", compactDisplay: "short" }) making big numbers like 104362432 become just 104.4M.

Please sign in to comment.