2014년 1월 29일 수요일

java awt 에서 문자열 출력

GUI를 위한 프로그램을 구성할 때 언제나 좌표와 크기 같은 것을 제어해 주어야 하기 때문에 번거 롭다는 생각이 듭니다.

특히 문자열을 출력할 때 그러한 경향이 더 나타나는 것 같습니다.

java.awt.Graphics 또는 Graphics2D class drawString 메소드는 문자열을 출력할 수 있는 메소드 입니다.

기본적인 drawString method 원형은 drawString(String str, int x, int y) 입니다.
여기서 잠깐 살펴보아야 할 부분은  x, y가 FontMetrics 의 baseline 을 기준으로 하고 있는 점입니다.

FontMetrics 객체는 Graphics 객체의 getFontMetrics() 메소드로 가져올 수 있습니다.

FontMetrics의 baseline은 영문자의 예로 Pig 라는 단어가 있을 때 대문자 P와 소문자 i 으 하단을 의미한다고 보시면 됩니다. g 는 baseline 아래에 약간의 영역에 출력하고 있습니다.
Font 에서 보면 baseline을 기준으로 그 위를 getAscent() ,아래를 getDescent() 메소드로 얻어 올 수 있습니다. 하나더 getLeading() 이라고 하여 line 사이의 값을 가져 올 수 있습니다.   하지만 api 에 언급되어 있듯 어떤 문자는 위에서 가져온 값과 달리 표기 될 수도 있습니다.  대략적인 값으로 이해 하면 될 것 같습니다.

그럼 일반적인 메소드 처럼 원하는 위치에 출력하고 싶다면 일반적으로 다음과 같은 방식을 쓸 수 있습니다.
String str = "msg";
int x   = 0;
int y   = 0;
int fontHeight = ((Graphics)gr).getFontMetrics().getHeight();

getHeight() 메소드는 일반적으로 ( getAscent() + getDescent() + getLeading() ) 의 크기 입니다.

y += fontHeight;

글꼴, style, 크기 는 Font class 를 통해 변경할 수 있습니다.
Font 는 현재 폰트로 부터 유도해서 만들 수도 있고 새롭게 생성해서 설정 할 수도 있습니다.
예를 들어 현재 폰트에서 크기와 스타일을 변경 하고 싶다면 다음과 같이 할 수 있습니다.
(gr 은 Graphics or Graphics2D 객체 )

int styleType = Font.BOLD | Font.ITALIC;
float pointSize = 12.0f;
Font defFont = gr.getFont();
Font derivedFont = defFont.deriveFont(styleType, pointSize);
gr.setFont(derivedFont);

만약 영역을 정해서 수직 수평 정렬을 하고 싶다면 앞서 언급한 내용을 이용 하시면 됩니다.
시작점인 x,y 가 있고 크기가 clipWidth, clipHeight 라고 한다면
String msg = "Test Program";

FontMetrics fm = gr.getFontMetrics();
int ftSize = fm.stringWidth(msg);

int nx = x +  (clipWidth-ftSize)/2;
int ny = (y-clipHeight+((clipHeight-(fm.getAscent()))/2));

gr.drawString(msg, nx,y);

여기서 getAscent() 를 이용하는 이유는 Font의 위치를 설정하기 위해서는 baseline 위의 영역만을 대상으로 하는게 중앙정렬처럼 보이기 때문입니다.

만약 수직 수평 정렬을 변경하고 싶다면 위의 nx, ny 값 계산을 약간만 변형하면 됩니다.

정렬은 위의 방식으로 할 수 있다고 해도 크기를 제한해서 크기가 넘어가면 안보이게 하고 싶다면, 위의 계산을 변형해서 해도 되지만, clip을 이용하는게 더 직관적일 것 같습니다.

위의 예에서 본다면
먼저 gr.setClip((x-1), ny, clipWidth, clipHeight) 를 구성 하시면 됩니다.
그 다음 gr.drawString(msg,nx,y); 를 호출한다면 영역을 넘어서는 문자는 출력되지 않습니다.
x-1을 한 이유는 clip 영역에 line을 그릴 경우 글자와 겹치는 부분을 피하기 위함 입니다. 정렬등의 계산에서는 그 보정치를 적용해 주시면 될 것 같습니다.

혹시 x,y의 포지션은 그대로 두고 클립영역을 기준으로 그 영역에서 중앙값을 조정하시려면
clip 영역은 단순하게 new Rectangle(x,y-clipHeight, clipWidth, chlipHeight) 의 사각형을 만들고, 다만 drawString의 x,y position 을 변경해 주시면 됩니다.
위에서 계산한 것을 예를 들면
x = nx;
y -= ((clipHeight-(fm.getAscent()))/2);
drawString(msg,x,y);

경우에 따라 문자열을 다양한 각도로 출력하는 경우가 발생할 수 있습니다.
그럴 때는 여러방법이 있겠지만, 다음의 두가지 방법중 편한 방법으로 각도를 조정할 수 있습니다.

먼저 Font 를 생성할 때 rotate 된 Font 를 사용하는 방법입니다.
일반적인 각도입니다.

double degree = 45.0;
AffineTransform affine = new AffineTransform();
affine.rotate(Math.toRadians(degree));
Font   derivedFont = gr.getFont().deriveFont(affine);
gr.setFont(derivedFont);
gr.drawString(msg,x,y);

다음은 Graphics 객체 자체를 rotate 시키는 방법입니다.
gr.rotate(Math.toRadians(degree), x, y);
gr.drawString(msg,x,y);

이 방법은 그리는 객체 전체를 움직인 것이라서 그후 출력되는 모든 상은 변경된 내용을 기준으로 하게 됩니다.

문자열의 품질은
gr.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
gr.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
gr.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
등을 적용하여 확인해 보실 수 있습니다.



java awt 혹은 swing 을 이용해서 문자열을 출력할 때 사용할 수 있는 방법을 간략히 살펴 보았습니다.

변경된 내용은 Graphics 객체에 계속 영향을 미치기 때문에 다른 그림 혹은 문자를 출력할 때에도 설정 내용이 적용됩니다.   그렇기 때문에 혹시 원래대로 환원시킬려면 변경전 상태를 저장하고 있다가, 다 출력후 초기 상태로 돌려 놓는 것이 필요할 것 같습니다.














2014년 1월 21일 화요일

apache POI 에 대한 간략한 소개 및 구현 ( excel 중심 )

web 기반 프로젝트에서는 데이터를 다운 받고자 할 때, 특별한 reporting tool 을 사용하지 않는다면, excel 형식으로 데이터를 구성하여 다운 받는 경우가 종종 있는 것 같습니다.

그럴때 사용하기 좋은 open source가 apache POI 입니다.
apache POI는  MS Office 전체를 대상으로 하고 있는 만큼, excel 구성만을 위해 사용한다면 다소 무거운 측면도 있지만, 그 만큼 강력하고 다양한 기능을 제공 하고 있습니다.

프로그램은 다음의 사이트에서 http://poi.apache.org/ 내용을 확인하신 후 download 항목에서 필요한 내용을 받으시면 됩니다. ( 글쓰는 시점의 안정버전은 POI 3.9 입니다. )

POI를 활용하여 excel 파일을 생성할 때 2003 version 의 *.xls, 2007 이상의 *.xlsx 파일 생성이 가능합니다.  2003 version 은 column 갯수의 제한(256) 이 있기 때문에 반듯이 구현해야 할 경우가 아니라면 xlsx 파일 형식으로 구성하는 것이 좋을 것 같습니다.
xls 파일은 HSSFWorkbook 클래스를 이용하고, xlsx 파일은 XSSFWorkbook class 를 생성하여 구성합니다.  

poi 는 excel 기능의 sheet 생성, cell 의 data type, style 에 의한 꾸미기 기능 및
shape 을 구성하여 그리기 기능을 제공하거나, image ( jpeg,png,dib )를 sheet 내에 구성할 수도 있습니다.

api와 설명을 보면 cell merge나 수식, 합계 등의 기능등 excel 에서 제공하는 많은 기능들이 거의다 구현 가능한 것으로 보입니다.


이러한 기능중 excel 문서 생성기능을 중심으로 간략하게 예제를 구성해 보았습니다.



테스트용도의 코드 입니다.


public static void makeExcelWorkbookFileTest(String fileName) {


Map<String,Map<String,List<String>>> excelDataMap = new LinkedHashMap<String,Map<String,List<String>>>();


Random rd = new Random();

Map<String,List<String>> dataListMap = new LinkedHashMap<String,List<String>>();
for ( int y = 0; y < 20; y++ ) {
for ( int x = 0; x < 18; x++ ) {
String titleStr = "Title_FIRST_" + x ;
List<String> dataList = dataListMap.get(titleStr);
if ( dataList == null ) {
dataList = new ArrayList<String>();
dataListMap.put(titleStr,dataList);
}
if ( x % 2 == 0 ) {
dataList.add((Math.round(x*100000*rd.nextDouble())/100.0)+"");
} else {
dataList.add("X_" + x);
}
}
}
excelDataMap.put("TEST_FIRST_SHEET",dataListMap);
dataListMap = new LinkedHashMap<String,List<String>>();
for ( int y = 0; y < 20; y++ ) {
for ( int x = 0; x < 18; x++ ) {
String titleStr = "Title_SECOND_" + x ;
List<String> dataList = dataListMap.get(titleStr);
if ( dataList == null ) {
dataList = new ArrayList<String>();
dataListMap.put(titleStr,dataList);
}
if ( x % 1 == 0 ) {
dataList.add((Math.round(x*100000*rd.nextDouble())/100.0)+"");
} else {
dataList.add("X_" + x);
}
}
}
excelDataMap.put("TEST_SECOND_SHEET",dataListMap);


Workbook wb = getExcelWorkbook(excelDataMap);

File f = new File(fileName);
if ( f.exists() ) {
f.delete();
}
FileOutputStream fOut = null;
try {
fOut = new FileOutputStream(f);
wb.write(fOut);
fOut.close();
fOut = null;
} catch (FileNotFoundException fne) {
fne.printStackTrace();
} catch ( IOException ioe ) {
ioe.printStackTrace();
} catch ( Exception ee ) {
ee.printStackTrace();
} finally {
if ( fOut != null ) {
try {
fOut.close();
} catch ( Exception eee ) {}
fOut = null;
}
}

}

//    excel work bool을 생성하여 반환하기 위한 메소드 입니다.
//    데이터를 담고 있고 간단한 style적용을 위한 부분을 추가해 보았습니다.

public static Workbook getExcelWorkbook(Map<String,Map<String,List<String>>> excelDataMap) {
if ( excelDataMap == null || excelDataMap.size() == 0 ){
return null;
}
Workbook wb = new XSSFWorkbook();
boolean flag = false;

CellStyle titleStyle = wb.createCellStyle();
titleStyle.setBorderBottom(CellStyle.BORDER_THIN);
   titleStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex());
   titleStyle.setBorderLeft(CellStyle.BORDER_THIN);
titleStyle.setLeftBorderColor(IndexedColors.GREEN.getIndex());
   titleStyle.setBorderRight(CellStyle.BORDER_THIN);
   titleStyle.setRightBorderColor(IndexedColors.BLUE.getIndex());
   titleStyle.setBorderTop(CellStyle.BORDER_MEDIUM_DASHED);
titleStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());
titleStyle.setAlignment(CellStyle.ALIGN_CENTER);
titleStyle.setFillForegroundColor(IndexedColors.SEA_GREEN.getIndex());
titleStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
titleStyle.setWrapText(true);

CellStyle contentStyle = wb.createCellStyle();
contentStyle.setBorderBottom(CellStyle.BORDER_THIN);
   contentStyle.setBottomBorderColor(IndexedColors.BLUE.getIndex());
   contentStyle.setBorderLeft(CellStyle.BORDER_THIN);
contentStyle.setLeftBorderColor(IndexedColors.BLUE.getIndex());
   contentStyle.setBorderRight(CellStyle.BORDER_THIN);
   contentStyle.setRightBorderColor(IndexedColors.BLUE.getIndex());
   contentStyle.setBorderTop(CellStyle.BORDER_THIN);
contentStyle.setTopBorderColor(IndexedColors.BLUE.getIndex());
contentStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
contentStyle.setFillPattern(CellStyle.SPARSE_DOTS);

for ( String sheetName : excelDataMap.keySet() ) {
if ( sheetName == null || sheetName.trim().length() == 0 ) {
continue;
} else {
flag = true;
}
Map<String,List<String>> dataListMap = excelDataMap.get(sheetName);
Sheet excelSheet = wb.createSheet(sheetName);
createExcelSheet(excelSheet,dataListMap,titleStyle,contentStyle);
}

if ( !flag ){
return null;
}
return wb;
}

//     sheet 의 내용을 구성하기 위한 메소드 입니다.

private static boolean createExcelSheet(Sheet excelSheet
, Map<String,List<String>> dataListMap
,CellStyle titleStyle, CellStyle contentStyle ) {
int logCount = 0;
boolean result = false;
if ( excelSheet == null ){
return false;
}

if ( dataListMap == null || dataListMap.size() == 0 ) {
Row excelRow = excelSheet.createRow(0);
Cell excelCell = excelRow.createCell(0);
return true;
}
int columnIndex = 0;
int rowIndex = 0;
int rowSize = 0;
List<String> keyOrderList = new ArrayList<String>();
Row excelTitleRow = excelSheet.createRow(0);

for ( String keyStr : dataListMap.keySet() ) {
if ( keyStr == null || dataListMap.get(keyStr) == null || dataListMap.get(keyStr).size() == 0 ) {
continue;
keyOrderList.add(keyStr);
rowSize = (rowSize < dataListMap.get(keyStr).size() ? dataListMap.get(keyStr).size() : rowSize);
Cell excelCell = excelTitleRow.createCell(columnIndex++);
excelCell.setCellValue(keyStr);
if ( titleStyle != null ) {
excelCell.setCellStyle(titleStyle);
}
}

if ( columnIndex == 0 ) {
Cell excelCell = excelTitleRow.createCell(0);
return true;
}

rowIndex = 1;
for ( int y = 0; y < rowSize; y++ ) {
Row excelRow = excelSheet.createRow((rowIndex+y));
for ( int x = 0; x < columnIndex; x++ ) {
String valueStr = null;
if ( dataListMap.get(keyOrderList.get(x)).size() > y ) {
valueStr = dataListMap.get(keyOrderList.get(x)).get(y);
Cell excelCell = excelRow.createCell(x);
if ( valueStr != null ) {
if ( isNumberFormatStr(valueStr) ) {
excelCell.setCellValue(Double.valueOf(valueStr));
} else {
excelCell.setCellValue(valueStr);
}
}
if ( contentStyle != null ) {
excelCell.setCellStyle(contentStyle);
}
}
}
return result;
}

//    정규식 글에서 사용한 메소드 입니다.
private static boolean isNumberFormatStr(String str) {
if ( str == null ){
return false;
}
return str.matches("[+\\-]?([\\d]+([.][\\d]*)?|[.][\\d]+)([eE][+\\-]?[\\d]+)?");
}


앞서 소개한 사이트에는 다양한 예제가 있습니다.
이곳에서 간략하게 구현한 메소드는 excel download 기능을 구현할 때 사용할 수 있도록 데이터 구성과 sheet 구성에 초점을 맞추어 구성해 보았습니다.

Workbook 클래스의 write 메소드의 argument 는 OutputStream 입니다.  Servlet 으로 구성할 때는 content type을 설정해 주시고, ServletOutputStream 객체를 넘겨 주시면 됩니다. 다만 위 소스에서는 stream 의 close를 명시적으로 해 주었지만, container 에서 close 를 자동관리하는 경우 close 메소드를 명시적으로 선언하지 않는 것이 좋습니다. 사용하고 있는 framework 혹은 container 에서 후처리 과정에서 stream 객체를 이용하여 무엇인가를 하고 있다면, close 메소드나, null 처리는 문제를 일으킬 수 있기 때문입니다.   






















2014년 1월 17일 금요일

그래프 구성을 위한 간략한 Marching Squares 구현

여러 분야에서 이용할 수 있겠지만, 데이터 시각화를 위한 그래프를 구성하는 관점에서 볼때 marching squares 에 대한 부분은 contour plot을 구성할 때 긴요하게 사용할 수 있을 것 같습니다.

부드럽고 정교한 plot을 위해서는 더욱 깊은 생각과 학습이 필요하겠지만, 기본적인 내용은 이를 바탕으로 구성하여도 크게 문제가 될 것 같지는 않습니다.

Marching Squares 에 대한 내용은 영문사이트이긴 하지만, http://en.wikipedia.org/wiki/Marching_squares 에서 상세한 내용을 확인하실 수 있으실 겁니다.

간단하게 보면 사각형을 중심으로 어느 지점에 데이터가 있는지에 따라 영역을 분할 하는 방법이라고 생각하면 될 것 같습니다.
4각형이니까 각 꼭지점을 기준으로 있을 경우 왼쪽 상단에서 시계방향으로 8, 4, 2, 1 의 값을 설정하겠습니다.

데이터의 수준을 '2'를 기준으로 아래의 원 배열을 [8,4,2,1] 배열로 찾아 보겠습니다.

    1  1  1  1           8  4            2   3   1  
    1  2  2  1   ->    1  2   - >     4  12  8
    1  1  1  1
왼쪽이 원본, 중앙은 꼭지점 기준값, 오른쪽은 원본을 꼭지점 유무로 재 구성한 배열 입니다.
오른쪽 배열은 원본배열에서 row, column 이 하나씩 작습니다.
처음 위치에서 보면 [8,4,2,1] 의 순서를 기준으로 볼 때 오른쪽 하단에 위치한 '2'이외는 모두 '1'입니다. 그래서 처음값은 2가 됩니다.
두번째 위치(x 가 하나증가된 위치 )에서  보면 하단 영역에 두개의 '2' 가 위치해 있습니다. 1|2 의 값인 3을 저장합니다. 이런식으로 구성하면 위의 오른쪽 배열의 값을 도출 할 수 있습니다.

다음은 위의 값을 기준으로 그래프 위치를 구성할 순서입니다.
1+2+4+8 = 15 입니다.(1|2|4|8). 네! 나올 수 있는 경우의 수는 15입니다.
1,2,4,8 의 값은 각 꼭지점에 값이 있다는 의미니까 꼭지점의 4 귀퉁이 삼각형을 생각할 수 있습니다.
11(1|2|8)은 4와 라인은 같지만 차지하는 면적은 4영역 이외의 모든 영역입니다.
14(2|4|8)은 1과 라인은 같지만 반대 영역을 차지하고 있습니다.
13(8|4|1), 7(1|2|4) 도 위와 같은 방식으로 생각해 볼 수 있습니다.
3(1|2),12(8|4) 는 아래 혹은 위를 기준으로 4각형을 구분짖는 것을 생각해 볼 수 있습니다.
9(1|8),6(2|4)은 좌우로 구분된 직사각형 영역을 생각해 볼 수 있습니다.
15(1|2|4|8) 모든 영역이 포함되어 있습니다.
다만 5, 10의 경우는 다소 애매한 구석이 있습니다.
분절이 될 수도 있고 중앙을 통과하는 것으로 여길 수도 있습니다.
일반적으로 4곳의 평균값을 기준으로 근접한 값을 설정하는 것이 일반적 일 수 있겠지만 다중 레벨을 기준으로 할 때 대각선의 값을 기준으로 상대편 대각선의 값이 같다면 누구의 영역도 아니게 하고 다르다면 통과하게 하는 방법을 사용해 보았습니다.  polygon 으로 구성할 때 경우에 따라서는 빈 공간이 발생할 수도 있으나 예제로서는 의미가 있어서 구성해 보았습니다. 실제로는 그 마저 같은 경우 그 공간은 평균값으로도 애매한 영역에 있을 가능성이 높기 때문에 상위나 하위 레벨로 연결을 몰아 갈 수 있습니다.
하여간 그 부분을 해결하기 위해 21과 26의 값을 추가 하였습니다. 5+16 의 값과 10+16의 값을 추가한 것입니다.

아래는 소스 입니다.
테스트 용도의 소스이기 때문에 정돈되어 있지 않습니다.
또한 여러 클래스로 분화 중이기 때문에 역시 모두 기재하기는 어렵습니다.
핵심적인 구조로 보아 주시면 될 것 같습니다.

marching squares 영역을 구성하기 위한 메소드 입니다.

public static int[][] makeMarchingSquaresArray(int[][] orgArray, int matchLevelNum) {
int[][] result = null;
int[][] filter = new int[2][2];
filter[0][0] = 8;
filter[0][1] = 4;
filter[1][0] = 1;
filter[1][1] = 2;

if ( orgArray == null || orgArray.length == 0
|| orgArray[0] == null || orgArray[0].length == 0 ) {
return result;
}

int rowSize = orgArray.length;
int colSize = orgArray[0].length;

result = new int[rowSize-1][colSize-1];


for ( int i = 1; i < rowSize; i++ ) {
int py = i-1;
for ( int j = 1; j < colSize; j++ ) {
int px = j-1;
int v = 0;
for ( int t = 0; t < 2; t++ ) {
for ( int z = 0; z < 2; z++ ) {
if ( orgArray[py+t][px+z] == matchLevelNum ) {
v = (v|filter[t][z]);
}
}
}
if ( v == 5 ) {
if ( orgArray[py][px] == orgArray[py+1][px+1] ) {
v += 16;
}
}
if ( v == 10 ) {
if ( orgArray[py][px+1] == orgArray[py+1][px] ) {
v += 16;
}
}

result[py][px] = v;
}
}

return result;
}


다음은 위에서 구성한 값을 기준으로 라인 혹은 폴리곤을 구성하기 위한 좌표점을 계산하여 넘겨 주기 위한 메소드 입니다.
return 값의 GraphLinePoint 는 라인 좌표를 가지고 있는 데이터 형이라고 생각하시면 될것 같습니다.
위치는 정방형의 1.0 의 중간지점을 기준으로 하였습니다.
그 지점을 조정하시면 레벨간 선의 중복등을 조절 할 수 있습니다.

public static GraphLinePoint getLevelPoints(int x, int y, int levelNum) {
if ( levelNum <= 0 ) {
return null;
}
double x1, x2, x3, x4, y1, y2, y3, y4;
GraphLinePoint result = new GraphLinePoint();
switch ( levelNum ) {
case 1 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 1.0;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 2 :
x1 = 0.5;
y1 = 1.0;
x2 = 1.0;
y2 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 3 :
x1 = 0.0;
y1 = 0.5;
x2 = 1.0;
y2 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 4 :
x1 = 0.5;
y1 = 0.0;
x2 = 1.0;
y2 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 5 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 0.0;

x3 = 0.5;
y3 = 1.0;
x4 = 1.0;
y4 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
result.appendLinePoint(new GraphLeafPoint(x3+x,y3+y), new GraphLeafPoint(x4+x,y4+y));
break;
case 6 :
x1 = 0.5;
y1 = 0.0;
x2 = 0.5;
y2 = 1.0;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 7 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 0.0;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 8 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 0.0;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 9 :
x1 = 0.5;
y1 = 0.0;
x2 = 0.5;
y2 = 1.0;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 10 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 1.0;

x3 = 0.5;
y3 = 0.0;
x4 = 1.0;
y4 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
result.appendLinePoint(new GraphLeafPoint(x3+x,y3+y), new GraphLeafPoint(x4+x,y4+y));
break;
case 11 :
x1 = 0.5;
y1 = 0.0;
x2 = 1.0;
y2 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 12 :
x1 = 0.0;
y1 = 0.5;
x2 = 1.0;
y2 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 13 :
x1 = 0.5;
y1 = 1.0;
x2 = 1.0;
y2 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 14 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 1.0;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
break;
case 15 :

break;
case 21 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 1.0;
x3 = 0.5;
y3 = 0.0;
x4 = 1.0;
y4 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
result.appendLinePoint(new GraphLeafPoint(x3+x,y3+y), new GraphLeafPoint(x4+x,y4+y));
break;
case 26 :
x1 = 0.0;
y1 = 0.5;
x2 = 0.5;
y2 = 0.0;

x3 = 0.5;
y3 = 1.0;

x4 = 1.0;
y4 = 0.5;
result.appendLinePoint(new GraphLeafPoint(x1+x,y1+y), new GraphLeafPoint(x2+x,y2+y));
result.appendLinePoint(new GraphLeafPoint(x3+x,y3+y), new GraphLeafPoint(x4+x,y4+y));
break;
default :
break;
}
return result;
}


아래는 테스트 코드 입니다.

int rowSize = 6;
int colSize = 6;
int level = 5;

//      레벨영역을 구성하기 위한 부분 입니다.
//      외각을 2로 그 안은 1로 중심부를 3이라는 값으로 구성 하고 있습니다.

int[][] levelArray = new int[rowSize][colSize];
for ( int i = 0; i < rowSize ;i++ ) {
for ( int j = 0; j < colSize; j++ ) {
if ( i > 0 && i < rowSize-1 && j > 0 && j < colSize-1 ) {
if ( (i == 2 && ( j == 2 || j == 3 )) || (i == 3 && ( j == 2 || j == 3 ))  ) {
levelArray[i][j] = 3;
} else {
levelArray[i][j] = 1;
}
} else {
levelArray[i][j] = 2;
}
}
}

System.out.println("");
System.out.println("Level ");
for ( int i = 0; i < rowSize; i++ ) {
for ( int j = 0; j < colSize; j++ ) {
System.out.print("\t\t" + levelArray[i][j] );
}
System.out.println("");
}

//      레벨 1이라는 숫자의 영역만 marching squares 영역으로 구성하기 위한 메소드 입니다..
int[][] contourArray = makeMarchingSquaresArray(levelArray,1);

System.out.println("");
System.out.println("Contour ");
for ( int i = 0; i < rowSize-1; i++ ) {
for ( int j = 0; j < colSize-1; j++ ) {
System.out.print("\t\t" + contourArray[i][j] );
}
System.out.println("");
}

//      예제를 그림으로 출력하기 위한 영역입니다.  참고로 보아 주시면 될 것 같습니다.
int xSize = 820;
int ySize = 820;
BufferedImage bImg = new BufferedImage(xSize,ySize,BufferedImage.TYPE_INT_RGB);
Graphics2D gr = bImg.createGraphics();
gr.setColor( Color.WHITE );
gr.fillRect(10,10,800,800);
gr.setColor(Color.BLUE);
getContourLineData(contourArray);
for ( int i = 0; i < rowSize-1; i++ ) {
for ( int j = 0; j < colSize-1; j++ ) {
GraphLinePoint gLine = getLevelPoints(j,i,contourArray[i][j]);
if ( gLine == null ) {
continue;
}
List<List<GraphLeafPoint>> points = gLine.getLines();
if ( points != null && points.size() > 0  ) {
for ( List<GraphLeafPoint> pList : points ) {
if ( pList == null || pList.size() != 2 || pList.get(0) == null || pList.get(1) == null ) {
System.out.println ( j + " : " + i + " SKIP ");
continue;
}
try {
gr.drawLine(10+(int)pList.get(0).getTransX(colSize-1,800),10+(int)pList.get(0).getTransY(rowSize-1,800)
, 10+(int)pList.get(1).getTransX(colSize-1,800),10+(int)pList.get(1).getTransY(rowSize-1,800) );
} catch ( Exception ee ) {
ee.printStackTrace();
break;
}
}
}
}
}
gr.dispose();

java.io.File f = new java.io.File("test.png");
if ( f.exists() ) {
f.delete();
}
try {
javax.imageio.ImageIO.write(bImg,"png",f);
} catch ( Exception ee ) {
}

아래는 그 결과 입니다.

Level
2 2 2 2 2 2
2 1 1 1 1 2
2 1 3 3 1 2
2 1 3 3 1 2
2 1 1 1 1 2
2 2 2 2 2 2

Contour
2 3 3 3 1
6 13 12 14 9
6 9 0 6 9
6 11 3 7 9
4 12 12 12 8



데이터가 많아지면 위의 예제에서 보다는 보다 부드러운 곡선이 가능해 집니다.
더 부드러운 곡면이 필요하다면 보간법과 영역에 대한 경계처리 등에서 작업할 내용이 더 많아 질 듯 합니다.

랜덤으로 좀 많은 데이터를 출력해 보았습니다.  레벨은 5가지중 2개만 출력해 보았습니다.


지금 예시한 소스는 marching squares 를 활용하는 방법을 코드화 해 보았습니다.
기본적인 구현이라 에러나 예외에 대한 부분은 많이 생략하였습니다.

이미지로 구성하기 위한 객체 등은 다 올릴 수 없어 핵심적인 영역만 기재해 보았습니다.






2014년 1월 12일 일요일

2차함수 추정 및 구현

주어진 데이터가 3개의 값을 가지고 있고 그 'x' 위치의 값이 독립적이면 Lagrange Interpolation 을 이용하여 값을 구할 수 있습니다.

그런데 주어진 값이 4개 이상일 경우 그리고 그 함수가 2차함수 형식이라고 알고 있는 경우 추정을 하려면 다른 방법을 생각해 보아야 합니다.

이 글에서는 그 과정을 한번 생각해 보고자 합니다.
다항식 추정을 위해서는 보다 보편적인 규칙을 찾아야 하지만, 이번에는 구현과정을 가장 기초적인 미분과 연립방정식을 이용하여 추정해 보고자 합니다.
잘 모르기도 하지만, 생각을 코드화 하는 과정을 논의해 보고 싶다는 생각이 들었기 때문입니다.

일단 알 수 있는 것은 2차 함수의 미분으로 1차 함수를 구성할 수 있고 그 함수는 미분한 'x' 접선의 기울기라는 사실입니다.
예를 들어 'y=2x^2 - 4x + 8' 를 미분하면 '4x-4' 가 됩니다.  x가 1일때 기울기가 0 이되니 이 지점이 최저점이 될 것입니다.

좌표는 위 수식에 따라
x = { -8, -6, -4, -2, 0, 2, 4, 6, 8 },
y = {168.0, 104.0, 56.0, 24.0, 8.0, 8.0, 24.0, 56.0, 104.0}
라고 가정해 보겠습니다.
물론 추세선이기 때문에 좌표가 함수에 일치하지는 않겠지만 일단 위와 같은 위치로 가정해 보겠습니다.

먼저 살펴보아야 할 순서를 생각 해 보겠습니다.

1. x 좌표를 오름차순으로 정렬하고 y좌표를 그에 맞춰 정렬하기(정렬되어 있다고 가정)
2. 앞 뒤 두 좌표의 기울기 및 x의 평균값으로 1차함수(미분된 값-기울기)를 설정
3. 2차함수의 계수 및 1차 함수의 계수를 평균값으로 구하기
4. 2차 계수와 1차 계수 값으로 상수 값(평균)구하기
를 생각해 볼 수 있습니다.

먼저 x좌표값에 의한 정렬은 되어 있다고 가정하겠습니다.
위의 예에서 보면 두점 사이의 기울기는 두점의 평균값위치에서 기울기와 같을 것이라는 가정을 해 보았습니다.
(104.0-168.0)/(-6-(-8)) = -32, (-6+(-8))/2 = -7
1차 미분식을 a*x+b 로 가정하고 a 가 1차 계수 b가 상수라고 하면
이는 x값이 -7일 때 기울기가 -32 란 의미니까 결국 2차식의 미분함수인 a*x+ b = -32 란 의미고 이는 -7*a + b = -32 란 의미가 됩니다.

(56.0-104.0)/(-4-(-6)) = -24, (-4+(-6))/2 = -5
이 부분을 위와 같은 방식으로 구성하면,
-5*a + b = -24 라는 식으로 구성할 수 있습니다. 두 방정식을 풀기 위해 b 값으로 하나를 환원하면 b = -24 + 5*a 라는 식으로 만들 수 있고 이를 위에 대입하면
-7*a + (-24+5*a ) = -32 입니다. 풀어 보면 a = 4를 도출 할 수 있습니다. b의 값은 a=4라는 값에 의해서 -5*4 + b = -24, b = -4의 값이 나오게 됩니다.

주어진 좌표에서 이와 같은 식으로 a, b를 계산해서 저장한 다음 a의 평균값, b의 평균값을 선택하게 됩니다.

다음은 c를 풀기 위해서 원래 함수인 2차 함수로 환원하여(미분된 함수 이기 때문에 다시 원래 함수의 값으로 변환하기 위해 a/2*x^2+ b*x + c 의 형태로 구성합니다.
결국  a = 4/2 , a = 2의 값이 됩니다.

주어진 좌표에서 예를 들면
x = -8일 때 y = 168 이니까, -8^2*2 - 4*-8 + c = 168입니다. 128 + 32 + c = 168, c = 8 입니다.
이렇게 좌표에서 c를 다 구하고 평균값을 취하면 2*x^2 - 4*x + 8 의 식을 도출 할 수 있습니다.

다음은 구현한 코드 입니다.

public static List<Double> getQuadracticEquations(List<? extends Number> xList
, List<? extends Number> yList ) {

if ( xList == null || yList == null || xList.size() < 3 || xList.size() != yList.size() ){
return null;
}

int size = xList.size();
for ( int i = size-1; i >= 0; i-- ) {
if ( xList.get(i) == null || yList.get(i) == null
|| Double.isNaN(xList.get(i).doubleValue()) || Double.isNaN(yList.get(i).doubleValue())
|| Double.isInfinite(xList.get(i).doubleValue()) || Double.isInfinite(yList.get(i).doubleValue()) ) {
xList.remove(i);
yList.remove(i);
}
}

size = xList.size();
if ( size < 3 ) {
return null;
}

List<Double> result = new ArrayList<Double>();

double a = Double.NaN;
double b = Double.NaN;
double c = Double.NaN;

double preXAvg = Double.NaN;
double preSlope = Double.NaN;
List<Double> aCoefList = new ArrayList<Double>();
List<Double> bCoefList = new ArrayList<Double>();
List<Double> cCoefList = new ArrayList<Double>();

for ( int i = 1; i < size; i++) {
double xGap = xList.get(i).doubleValue()-xList.get(i-1).doubleValue();
double yGap = yList.get(i).doubleValue()-yList.get(i-1).doubleValue();
double slope = (yGap/xGap);
double xa = (xList.get(i).doubleValue()+xList.get(i-1).doubleValue())/2.0;
if ( !Double.isNaN(preXAvg) ) {
double aV = ((slope)- preSlope)/(xa - preXAvg);
double bV = (yGap/xGap)-aV*xa;
// System.out.println ( "\tRESULT : " + aV + " : " + bV);
aCoefList.add(aV);
bCoefList.add(bV);
}
preXAvg = xa;
preSlope = slope;
}

a = StatisticsBaseUtil.getAverageValue(aCoefList)/2.0;
b = StatisticsBaseUtil.getAverageValue(bCoefList);

for ( int i = 0; i < size; i++) {
cCoefList.add(yList.get(i).doubleValue() - (Math.pow(xList.get(i).doubleValue(),2)*a + xList.get(i).doubleValue()*b));
}
c = StatisticsBaseUtil.getAverageValue(cCoefList);
result.add(a);
result.add(b);
result.add(c);

return result;
}


테스트 용도로 두가지를 구성해 보았습니다.
먼저 검증용 데이터 입니다.

double a = 2.0;
double b = -4.0;
double c = 8.0;

List<Double> xList = new ArrayList<Double>();
List<Double> yList = new ArrayList<Double>();
int size = 10;
double xx = -8.0;

for ( int i = 0; i < size; i++) {
double x = xx+i*2; 
xList.add(x);
yList.add(Math.pow(x,2)*a+b*x+c);
}
System.out.println ( xList + "\n" + yList );
System.out.println ( getQuadracticEquations(xList,yList) );

결과는 아래와 같습니다.
[-8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
[168.0, 104.0, 56.0, 24.0, 8.0, 8.0, 24.0, 56.0, 104.0, 168.0]
[2.0, -4.0, 8.0] -> 계수들

다음은 데이터를 약간 흔들어 보았습니다.


double a = 2.0;
double b = -4.0;
double c = 8.0;

List<Double> xList = new ArrayList<Double>();
List<Double> yList = new ArrayList<Double>();
int size = 10;
double xx = -8.0;

Random rd = new Random();

for ( int i = 0; i < size; i++) {
double x = xx+i*2; 
xList.add(x);
yList.add(Math.pow(x,2)*a+b*x+c + rd.nextDouble()*10.0);
}
System.out.println ( xList + "\n" + yList );
System.out.println ( getQuadracticEquations(xList,yList) );

결과는 아래와 같습니다.
[-8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
[169.37547437750422, 111.00550252621369, 59.85251104741083, 30.723567117837423, 11.018439045994608, 15.543492759715203, 26.299058613035104, 61.63218678109505, 104.34192659530801, 175.75788040181308]
[2.027905088403056, -7.334878784772051, 14.941109705660864]

유사한 추세를 보여 주긴 하지만 무엇인가 조금 어긋나 보이는 것 같습니다.
그래서 조금 각도를 틀어 생각해 보기로 하겠습니다.

각 좌표값의 변화가 평균적으로만 적용되기 때문에 전체적으로는 튀는 값이 보다 많이 적용될 수 있다는 생각을 해 볼 수 있습니다.

미분이 1차 방정식이기 때문에 위에서 구한 x의 위치 'xa'와 slope 을 이용해서 선형 최소제곱법을 적용해 보겠습니다.


public static List<Double> getQuadracticEquations(List<? extends Number> xList
, List<? extends Number> yList ) {

if ( xList == null || yList == null || xList.size() < 3 || xList.size() != yList.size() ){
return null;
}

int size = xList.size();
for ( int i = size-1; i >= 0; i-- ) {
if ( xList.get(i) == null || yList.get(i) == null 
|| Double.isNaN(xList.get(i).doubleValue()) || Double.isNaN(yList.get(i).doubleValue()) 
|| Double.isInfinite(xList.get(i).doubleValue()) || Double.isInfinite(yList.get(i).doubleValue()) ) {
xList.remove(i);
yList.remove(i);
}
}

size = xList.size();
if ( size < 3 ) {
return null;
}

List<Double> result = new ArrayList<Double>();

double a = Double.NaN;
double b = Double.NaN;
double c = Double.NaN;

double preXAvg = Double.NaN;
double preSlope = Double.NaN;
List<Double> cCoefList = new ArrayList<Double>();
List<Double> xxList = new ArrayList<Double>();
List<Double> yyList = new ArrayList<Double>();


for ( int i = 1; i < size; i++) {
double xGap = xList.get(i).doubleValue()-xList.get(i-1).doubleValue();
double yGap = yList.get(i).doubleValue()-yList.get(i-1).doubleValue();
double slope = (yGap/xGap);
double xa = (xList.get(i).doubleValue()+xList.get(i-1).doubleValue())/2.0;
xxList.add(xa);
yyList.add(slope);
}
List<Double> leastList = StatisticsBaseUtil.getLeastSquaresValues(xxList,yyList);

a = leastList.get(0)/2.0;
b = leastList.get(1);

for ( int i = 0; i < size; i++) {
cCoefList.add(yList.get(i).doubleValue() - (Math.pow(xList.get(i).doubleValue(),2)*a + xList.get(i).doubleValue()*b));
}
c = StatisticsBaseUtil.getAverageValue(cCoefList);
result.add(a);
result.add(b);
result.add(c);

return result;
}

결과는 다음과 같습니다.

[-8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
[173.84227469091417, 106.62900546618599, 57.65556936199552, 32.05383516544042, 8.677565603651919, 8.091549432210913, 28.18148589027254, 62.75161780452815, 107.05732207887412, 173.9211479980657]
[2.0415744582349165, -4.078767066072524, 10.551372835299308]

또다른 방식으로 구한 계수는 다음과 같습니다.
[10.811528071220785, -4.012475708175465, 2.031973087828489]

그 결과를 R Graph 로 표현해 보겠습니다.


xGap 에 따른 오차등을 평균값으로 구성한 것에서 오류가 발생할 가능성이 있고, 그래서 그 오차를 줄이기 위해 계산한 x좌표와 slope을 x,y좌표로 재 구성하여 선형 최소제곱법을 수행해 보았습니다. 결과는 다른 방식으로 구한 계수값과 상당히 유사함을 보여 주고 있습니다.   x 사이값이 (평균값)이 해당 위치의 slope 이라는 가정은 위험하기는 하지만 직관적으로 보이기는 합니다.
3차, 4차 등으로 고차 함수로 추정해야 할 때 한계점도 있습니다.
다만, 이 글에서는 2차 함수로 추정하면서 코드화 하는 과정을 함께 살펴 보고 싶어서 구성한 글입니다.


다시 말씀 드리지만, 다른 추정방법과 그 결과값이 약간 다른 방법입니다.
코드의 StatisticsBaseUtil는 이전에 구성한 평균,분산,표준편차, 추세선에서 구현한 메소드 들입니다.







2014년 1월 10일 금요일

html 에서 엑셀 틀고정 구성하기

html 로 엑셀과 같은 그리드를 만드는 일은 그리 간단한 일은 아니지만, 그렇다고 안되는 일도 아닙니다.   다만, 상당히 번잡한 스크립트 코드와 디자인이 수반되어야 하는 시간이 걸리는 일이라고 생각합니다.

연산이 필요한 항목은 AJAX의 방법을 이용하여 서버에서 계산하게 한다면, 약간 복잡한 계산도 경우에 따라서는 차트관련한 내용도 구성할 수 있을 듯 합니다. (물론 클라이언트에서만 동작하도록 하여도 가능할 수는 있습니다.)  - 데이터가 많아 진다면 ... 한계는 있을 것 같습니다.

이 글에서는 html 에서 틀고정 그리드를 구성하는 방법에 대하여서만 생각해 보고자 합니다.

일단 div 로 그리드의 모든 영역을 구성할 지 아니면 table 구조를 사용할지는 결정하여야 합니다.  디자인과 더 많은 수고를 들일 용의가 있다면, div로 구성하는 것이 좋을지도 모르지만 개념적인 것을 살펴보기 위해 이 글에서는 table 구조를 사용합니다.   물론 틀은 div 로 구성할 예정입니다.

예전에 이런 저런 방법으로 구성해 본적이 있었는데 div 틀로 나누어 처리하는 것이 디자인뿐만 아니라 모듈을 구성하는데에도 훨씬 직관적인것 같습니다.  몇몇 사이트나 블로그에서도 그렇게 구성하는것 같기도 하고요 ...

아래의 내용은 다양한 기능을 모두 배제한 가장 기본적인 내용으로만 기술 하였습니다.

대략 6개의 div가 필요합니다.

첫번째 div 는 전체를 감싸는 grid div 입니다. padding, margin, border 등을 사용할 수 있게 하려면 그에 따른 크기조정을 미리 감안하여야 합니다.

두번째 div는 고정되어 움직이지 않는 영역을 구성합니다.  이 구성영역의 크기에 따라 왼쪽 영역과 오른쪽 영역의 각 넓이와 높이가 결정됩니다.

세번째 div 는 header 중 좌우로만 스크롤될 수 있는 영역입니다.
네번째 div 는 기능은 중요하지 않지만 디자인적으로 의미가 있는 scroll 할 때 사용하는 영역입니다. browser 에 따라 차이가 있지만, IE 에서는 17px 정도의 크기를 가지고 있습니다.

다섯번째 div 는 왼쪽에 고정되어 위 아래로 스크롤하는 div 입니다.
여섯번째 div 는 본문으로 좌우 상하 스크롤바가 달린 본문 영역입니다.

전체 감싸는 블록을 제외 하고는 div 의 style 에서 float:left 라는 항목이 있어야 합니다.

이런 6(내용은 실질적으로 4개)개로 나뉘어진 div에 table을 구성하여 그려 주면 틀고정 효과가 있는 내용을 생성할 수 있습니다.

아래와 같은 내용을 구성할 수 있습니다.

 <body onResize="return resizeGridSize();">
 <DIV class="__def__frame__block" id='gridBlock'>
     <DIV class="__def__header__fix__block" id='headerFixBlock'></DIV>
     <DIV class="__def__header__contents__block" id='headerCtnBlock'></DIV>
     <DIV class="__def__header__scroll__block" id='headerScrollBlock'> &nbsp; </DIV>          <DIV class="__def__main__fix__block" id='mainFixBlock'> </DIV>
     <DIV class="__def__main__contents__block" id='mainCtnBlock'> </DIV>
 </DIV>
 </body>

style 은 아래와 같습니다.

<style type="text/css">
html,body {
width:100%;
height:100%;
padding:0px;
margin:0px;
border:0px;
}

.__def__frame__block {
margin:0px;
padding:0px;
border:0px solid black;
width:100%;
height:100%;
background-color:#FFFF00;
overflow:hidden;
}

.__def__header__fix__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#FFFFFF;
overflow:hidden;
}

.__def__header__contents__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#FF0000;
overflow:hidden;
}

.__def__header__scroll__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
width:17px;
background-color:#999999;
overflow:hidden;
font-size:1px;
}

.__def__main__fix__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#009900;
overflow:hidden;
}

.__def__main__contents__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#000080;
overflow-x:scroll;
overflow-y:scroll;
scrollbar:17px;
}

.__def__main__contents__tbl {
table-layout:fixed;
padding:0px;
margin:0px;
border:0px;
text-align:center;
font-size:11px;
}

.__def__main__contents__tbl TD {
width:120px;
}
</style>

script 는 아래와 같습니다.

<script type="text/javascript">

function setScrollPosition() {
var hCtnBlock = document.getElementById("headerCtnBlock");
var mFixBlock = document.getElementById("mainFixBlock");
var mCtnBlock = document.getElementById("mainCtnBlock");

if ( !hCtnBlock || !mFixBlock || !mCtnBlock ) {
return false;
}

hCtnBlock.scrollLeft = mCtnBlock.scrollLeft;
mFixBlock.scrollTop = mCtnBlock.scrollTop;
}

function resizeGridSize() {
var mainGrid = document.getElementById("gridBlock");
var hFixBlock = document.getElementById("headerFixBlock");
var hCtnBlock = document.getElementById("headerCtnBlock");
var hSclBlock = document.getElementById("headerScrollBlock");
var mFixBlock = document.getElementById("mainFixBlock");
var mCtnBlock = document.getElementById("mainCtnBlock");

if ( !mainGrid || !hFixBlock || !hCtnBlock || !hSclBlock || !mFixBlock || !mCtnBlock ) {
alert("Grid 구성을 위한 객체를 찾을수 없습니다.");
return;
}

var mWidth = parseInt(mainGrid.clientWidth);
var mHeight = parseInt(mainGrid.clientHeight);
var hFixWidth = parseInt(hFixBlock.clientWidth);
var hFixHeight = parseInt(hFixBlock.clientHeight);

// browser 별 달리 설정 혹은 script 로 확인
var scrollSize = 17;

hCtnBlock.style.width = (mWidth-hFixWidth-scrollSize)+"px";
hCtnBlock.style.height = (hFixHeight)+"px";
hSclBlock.style.height = (hFixHeight)+"px";

mFixBlock.style.width =  (hFixWidth)+"px";
mFixBlock.style.height =  (mHeight-hFixHeight-scrollSize)+"px";

mCtnBlock.style.width =  (mWidth-hFixWidth)+"px";
mCtnBlock.style.height =  (mHeight-hFixHeight)+"px";

return false;
}

function appendEventObject(obj,type,fnHandler,isBubble) {
if ( !obj ) {
return false;
}
if ( obj.attachEvent ) {
obj.attachEvent("on"+type,fnHandler);
} else if ( obj.addEventListener ) {
obj.addEventListener(type,fnHandler,isBubble);
}
}

function makeRandomDistandMatrix(maxSize,connSize,maxDistance) {
// check arguments ... skip
var result = [];
var seed = Math.round(maxDistance/1000);
for ( var y = 0; y < maxSize; y++ ) {
var arr = [];
for ( var x = 0; x < maxSize; x++ ) {
var cPos = Math.round(x/2);
if ( (y-cPos) >= 0 && (y+cPos) < maxSize ) {
if ( x%2 == 0) {
cPos = y+cPos;
} else {
cPos = y-cPos;
}
} else if ( (y-cPos) < 0 ) {
cPos = x;
} else {
cPos = (maxSize-x-1);
}

if ( cPos == y ) {
arr[cPos] = 0;
} else {
if ( x < connSize ) {
arr[cPos] = Math.round(Math.random()*seed)*x;
} else {
arr[cPos] = maxDistance;
}
}
}
result.push(arr);
}
return result;
}

function drawTableTmp(matrixTbl,parentObj,classNameStr) {
var tableObj = document.createElement("TABLE");
var size = matrixTbl.length;
var cSize = matrixTbl[0].length;
tableObj.style.width = cSize*120+"px";
//parentObj.appendChild(tableObj);
tableObj.className = classNameStr;
tableObj.style.backgroundColor = "#000080";
tableObj.setAttribute("cellSpacing","1");
tableObj.setAttribute("cellPadding","0");
var tBodyObj = document.createElement("TBODY");
tableObj.appendChild(tBodyObj);
for ( var y = 0; y < size; y++ ) {
var trObj = document.createElement("TR");
trObj.style.height = "22px";
tBodyObj.appendChild(trObj);
for ( var x = 0,xSize=matrixTbl[y].length; x < xSize; x++ ) {
var tdObj = document.createElement("TD");
trObj.appendChild(tdObj);
tdObj.style.backgroundColor = "#FFFFFF";
tdObj.innerText = matrixTbl[y][x];
}
}
parentObj.innerHTML = tableObj.outerHTML;
}


function initResource() {
var hColumnSize = 3;
var hRowSize = 3;
var maxSize = 50;
var connSize = 8;
var maxDistance = 99999999;
var matrix = makeRandomDistandMatrix(maxSize,connSize,maxDistance);

var hFixMatrix = [];
var hCtnMatrix = [];
var mainLeftMatrix = [];
for ( var i = 0; i < hRowSize; i++ ) {
hFixMatrix[i] = [];
hCtnMatrix[i] = [];
for ( var j = 0; j < hColumnSize; j++ ) {
hFixMatrix[i].push("H_F_" + i + "_" + j);
}

for ( var j = 0; j < maxSize; j++ ) {
hCtnMatrix[i].push("H_C_" + i + "_" + j);
}
}

for ( var i = 0; i < maxSize; i++ ) {
mainLeftMatrix[i] = [];
for ( var j = 0; j < hColumnSize; j++ ) {
mainLeftMatrix[i].push("NUM_"+i + "_" +j);
}
}

var hFixBlock = document.getElementById("headerFixBlock");
var hCtnBlock = document.getElementById("headerCtnBlock");
var hSclBlock = document.getElementById("headerScrollBlock");
var mFixBlock = document.getElementById("mainFixBlock");
var mCtnBlock = document.getElementById("mainCtnBlock");

drawTableTmp(hFixMatrix,hFixBlock,"__def__main__contents__tbl");
drawTableTmp(hCtnMatrix,hCtnBlock,"__def__main__contents__tbl");
drawTableTmp(mainLeftMatrix,mFixBlock,"__def__main__contents__tbl");
drawTableTmp(matrix,mCtnBlock,"__def__main__contents__tbl");

resizeGridSize();
appendEventObject(document.getElementById("mainCtnBlock"),"scroll",setScrollPosition,false);
}

window.onload = initResource;

</script>

table은 임의로 4개의 영역으로 구성하였습니다.
영역이 구성되면 div를 resize 해서 스크롤이 필요한 영역을 활성화 합니다.
본문 영역에만 scroll event 를 구성하여 필요한 화면이 나오도록 구성하였습니다.

contenteditable 속성과 chart 기능을 추가하고 event handling 과 디자인적 요소를 더한 다면 ... (할게 너무 많은가요? )

구성된 코드는 IE와 Chrome 에서 동작합니다. 다만 scrollbar size 때문에 크롬에서는 디자인이 약간 깨지고 있습니다.

크롬에서 resize 이벤트가 잘 작동하지 않기 때문에 body 부분에 이벤트를 걸어 놓았습니다.

GWT, SmartGWT 등에서는 Grid 유형에 대한 지원이 많습니다.
자유로운 틀고정 까지는 아니라도 사용자가 Column의 크기를 조정하거나, 소팅기능을 제공하거나 하는 등요 .. 물로 Client 단에서 처리하는 내용이 많아지면 많아질 수록 반응속도는 느려지기 마련이고요 ....


관심있는 분에게 조금이라도 도움이 되었으면 합니다.